DEV Community

Ruben Suet
Ruben Suet

Posted on • Originally published at rubensuet.com

React Composition

In a previous post, I talked about React Context, a native alternative to sharing state like Redux or Mobx, or just to avoiding prop drilling. These other state sharing solutions imply time and effort to set-up and wire the state with your components. On the other hand with prop drilling you get a quick and straightforward solution without setting anything up. And together with this comes a simple, effective, and somehow not known pattern for it: React composition.

This posts motivation comes from a tweet from @mjackson which received many comments giving counter arguments to his idea. In response he decided to do a very nice explicative video about react composition which I recommended you watch as well. Now my intention with this post is to aggregate to these concepts exposed previosuly by mjackson without losing the initial reference. So let's continue on.

What is React Composition

React composition is one of the most basic techniques that we learn when we are working with react. It is mainly the children keyword. This example (for learning purposes) illustrates this pattern.

import * as React from 'react'

function DisplayCounter ({ children }) {
    return (
        <React.Fragment>
            <h1> My own counter </h1>
            <strong> Press the button and see the counter in action </strong>
            { children }
        </React.Fragment>
    )
}

function Counter () {
    const [counter, setCounter] = React.useState(0)
    const increase = setCounter(prevCounter => prevCounter +1)

    return (
    <React.Fragment>
        <DisplayCounter>
            <p> You have pressed { counter } times </p>
        </DisplayCounter>
        <button onClick={increase}> Increase! </button>
    </React.Fragment>
    )
}

 

I could have passed the counter prop into DisplayCounter and would have had no necessity to nest children here but imagine you now have a flow like this:

Counter (state is set) => anotherComponent => ... => displayCounter (consume states).
As you can see, you are now sending the prop through 2 or more components. It can easily be nested but now all of them have a strong dependency on that prop that they don't even use and are just passing it into the next component.
You could set up a React Context (or any state management library) but this solution is straightforward and gives me the benefit I was looking for.

Some real example

A header is often a component we can find in many web apps.

Asset Menu

I need to pass User info into 2 places: The avatar itself, and the settings dropdown.Imagine we got this Tree component

Asset Tree Component
The Header component is taking care to receive user info, and it spreads through the rest of the tree components.

In the classic approach, It would look something like:

import * as React from 'react'
import { fetchUser } from './someUtilsLibThatFetchesTheUser'
function Header () {
    const [user, setUser] = React.useState(undefined)
    React.useEffect(()=> {
        setUser(fetchUser())
    },[])
    return(
        <React.Fragment>
            <Avatar user={user} />
            <Menu user={user} >
        </React.Fragment>
    )
}

 

Not really nice to see so many user keywords. Moreover, if you make it with Typescript, you need multiple type definitions for that user object.

The idea here is to avoid prop drilling and make it easy.

import * as React from 'react'
import { fetchUser } from './someUtilsLibThatFetchUser'
function Header () {
    const [user, setUser] = React.useState(undefined)
    React.useEffect(()=> {
        setUser(fetchUser())
    },[])
    return(
        <React.Fragment>
            <Avatar>
                <img src={user.avatar} alt={user.username}>
            </Avatar>
            <Menu>
                { user ? <UserInfo user={user} /> : <LogIn/>
            </Menu>
        </React.Fragment>
    )
}

 

I am still sending one prop of the user, but now it's a single prop. Before probably would have been

Asset State flow between components

The Menu, without composition, would originaly be:

import * as React from 'react'

function Menu ({ user }) {
    return (
        <React.Fragment>
            { user ? <UserInfo user={user} /> : <LogIn />
            <Settings/>
        </React.Fragment>
    )
}

 

Why, if the Menu does not need anything from the user, does it still need to receive it and pass it into another component?

With Composition, the Menu would like this:

import * as React from 'react'

function Menu ({ children }) {
    return (
        <React.Fragment>
            {children}
            <Settings/>
        </React.Fragment>
    )
}

 

Here relies the power, the previous snippet you could like it more or less (No entendi esta primera oración :S). Depending on your situation you could need React Context or maybe even a more complex library, but sometimes only with Composition could do. This Menu snippet, shows us that the component doesn't need to know about the user object, it's not coupled at all. Children is as well a really powerful technique for Compound components, which I will explain in another post.

So a few more words about it: notice how we passed from sending the User into the Menu component, and from the Menu into the UserInfo component, to avoiding this 'proxy' and just passing the info and delegating it to the parent component, the Header.
For the Avatar, let's assume the component was just some stylings and was waiting for the image. No need to show some snippet there :)

Conclusion

In my opinion, React Composition is a great tool that can greatly help at the moment of developing. (React docs are encouraging you to use it). If what you put as children is not really huge, Composition is the best technique. Otherwise, if you're children take like 200 lines of code, I would consider another technique like Context. At the end it is just another card in your hand you must learn when it's best to play at a specific moment.

References for this post

See the original post at my blog suetBabySuet

Top comments (0)