DEV Community

Cover image for Using render callbacks to connect non-connected components in React
Michael Scott Hertzberg
Michael Scott Hertzberg

Posted on • Edited on

Using render callbacks to connect non-connected components in React

The difference between a connected and a non-connected component, is that a connected component is subscribing to some kind of state, whereas a non-connected component has no awareness of any state, therefore "dumb."

When developing scalable applications, you're more likely to create non-connected, "dumb" components, which may subsequently need to subscribe to some kind of state, like you would from a Redux store.

Let's imagine an ecosystem of components that make up a ticketing sales website. Many small components make up the application, but we'll focus on one: A module in the sidebar that shows the top ticket sales of the hour. The footer also has a module that shows the same data (It's actually the same exact module, we've just rendered a second instance of it). Both of these components are being instantiated from different places, but they require the same data. Both are dumb and both require data in order to render.

The idea behind a "provider" component would be to provide values. For example, we can create a functional component that takes a function as its child, and when called back, returns a property named data, that is an empty array. if you're unfamiliar with the concept of render callbacks, read my previous article, Power up your renders with render callbacks in React.

const Provider = (props) => {
    const data = []
    return props.children({ data })
}

With the above, we can use it the way we would use a render callback:

<Provider>
    {({ data }) => <MyComponent data={data} />}
</Provider>

What we've done is simply wrap our component within this Provider component, which will provide data, for the inner component (MyComponent) to use. Data can be anything we want, and come from anywhere. The provider can be renamed something else to be more descriptive:

<TopSalesByHour>
    {({ data }) => <Tickets data={data} />}
</TopSalesByHour>

The above provider simply acts as a state machine and provides state to its children, in the above case, Tickets. We can go a step further and provide a loading interface to Tickets, as we may want to wait for data before actually rendering Tickets:

<TopSalesByHour>
    {({ data }) => data ? <Tickets data={data} /> : <Loading />}
</TopSalesByHour>

What makes the above powerful, is that we've abstracted the data away from our view logic. We've also provided a scalable way to provide state to a stateless component. Lastly, we've provided our component the ability to conditionally render, based on that state.

We can go another step further into a scenario where we may want to provide multiple streams of data to a subscriber. In this case, not only do we want the Top Sales By Hour, but also Top Sales By Day. How can we "compose" these two providers?

/*
 * We need to combine both of these...
 */
<TopSalesByHour>
    {({ data }) => data ? <Tickets data={data} /> : <Loading />}
</TopSalesByHour>

/*
 * into one
 */
<TopSalesByDay>
    {({ data }) => data ? <Tickets data={data} /> : <Loading />}
</TopSalesByDay>

In order to do this, we need to create a helper function. Javascript isn't magic (sometimes it is). This helper function is going to accept providers as arguments and combine them into two readable values. Let's first visualize how we'd use the component:

<TopSales>
    {({byHour, byDay}) => (
        <React.Fragment>
            <Tickets data={byHour} />
            <Tickets data={byDay} />
        </React.Fragment>
    )}
</TopSales>

In the above case, rather than naming our variable data, we've given it a more descriptive name of what the data point actually is, since we're now going to compose both TopSalesByHour and TopSalesByDay by creating a TopSales component, which will combine the two:

const Create = (jsx, container) => (...containers) => ({ children }) => {
  const wrap = props =>
    typeof children === "function" // For react, in preact this is an array.
      ? children(props)
      : children.map(fn => fn(props));

  const gatherComponents = (parent, ...rest) => {
    const child =
      rest.length === 0
        ? props => jsx(container, null, wrap(props))
        : gatherComponents(...rest);

    return outerProps =>
      jsx(parent, outerProps, innerProps =>
        jsx(child, Object.assign({}, outerProps, innerProps))
      );
  };

  return gatherComponents(...containers)();
};

The above function was borrowed from render-prop-composer by mini-eggs. I like this one because of its simplicity. Let's use it and then explain how it works:

const composer = Create(React.createElement, React.Fragment)
const TopSales = composer(TopSalesByHour, TopSalesByDay)

As simple as that, we're now able to combine both TopSalesByHour and TopSalesByDay into a single TopSales component, whos sole purpose is just to feed us ticket data:

<TopSales>
    {({byHour, byDay}) => (
        <React.Fragment>
            <Tickets data={byHour} />
            <Tickets data={byDay} />
        </React.Fragment>
    )}
</TopSales>

But how exactly does the code work? Let's explore.

const Create = (jsx, container) => (...containers) => ({ children }) => {
  const wrap = props =>
    typeof children === "function" // For react, in preact this is an array.
      ? children(props)
      : children.map(fn => fn(props));

  const gatherComponents = (parent, ...rest) => {
    const child =
      rest.length === 0
        ? props => jsx(container, null, wrap(props))
        : gatherComponents(...rest);

    return outerProps =>
      jsx(parent, outerProps, innerProps =>
        jsx(child, Object.assign({}, outerProps, innerProps))
      );
  };

  return gatherComponents(...containers)();
};

The first thing to note with the above Create function, is that the first thing it does is return a function. This is why upon first use, we're setting up the function to use React.createElement and React.Fragment. I actually admire the author of the function for keeping the code agnostic and dependency free, whereby we're passing React functions for it to use internally. As you read further, the variable jsx will refer to React.createElement and container will refer to React.Fragment.

const composer = Create(React.createElement, React.Fragment)
const TopSales = composer(TopSalesByHour, TopSalesByDay)

When we subsequently pass the components that we want to compose, gatherComponents is called, using its arguments: TopSalesByHour and TopSalesByDay.

gatherComponents(...containers)();

It's within the above method that the magic happens:

  const gatherComponents = (parent, ...rest) => {
    const child =
      rest.length === 0
        ? props => jsx(container, null, wrap(props))
        : gatherComponents(...rest);

    return outerProps =>
      jsx(parent, outerProps, innerProps =>
        jsx(child, Object.assign({}, outerProps, innerProps))
      );
  };

Remembering that jsx is referring to React.createElement, the above code is returning another function (this is important) which is responsible for creating a new component (TopSales) and assigning it the props of each (...rest) component passed as arguments to composer (const TopSales = composer(TopSalesByHour, TopSalesByDay)).

Also, notice that gatherComponents is being called inside itself, denoting that this is a recursive function, whereby rest.length === 0 is the base case.

As gatherComponents iterates over each of the composed TopSales* components, outerProps holds the value for each of the iterated components holding our props data, which is assigned to the final composed component (TopSales) we'll be using.

That's all! At first glance, this may be confusing to the novice developer, but I urge you to try out the code in the sandbox linked below. If it still proves a challenge, I recommend reading up on closures and recursion.

If you have any questions, comments, concerns, thoughts, please leave a comment below! Also, schedule a session with me on Codementor and get your first 15 minutes free.


Try out the above psuedo code in real life on CodeSandbox: https://codesandbox.io/s/7j3pxr3xyq

References:

Top comments (0)