DEV Community

AVI
AVI

Posted on • Updated on

Use the React Context API; without nested render prop fatigue.

EDIT: with React Hooks, you can just use useContext to do this with no pain, this article is rendered of little value now, so is the library. I haven't found myself needing this at all.

A Little Context (lol)

Redux has been my home, will be my home for a lot of use cases. It's made life easy as a developer who's always had to single handedly manage projects of scale. But here's a myriad of use cases where you don't need Redux's magic or functionality. Sometimes you just need central state without prop drilling. I gave job interviews in the last year that required small take-home projects and I realised just how powerful the Context API can be when you don't need Redux/MobX et al. Only issue, Redux let me put everything in one place and elegantly select what I needed from it. With Consumers, I got stuck in situations where there were render props inside render props inside...you get the drift. If you're into functional programming, first thought in your mind is if only I could compose these. Let's look at some mildly problematic code to understand this.

import React, { Fragment } from "react";
import { render } from "react-dom";
import { __, map, prop } from "ramda";

import Drawer from 'drawer-component-from-wherever';
import List from 'list-component-from-wherever';
import Title from 'title-component-from-wherever';


/*
    Note: the following is not the "right" way to initialise context values, you're
    supposed to use a Provider and pass a value prop to it. If the Consumer finds
    no matching parent Provider, only then it uses the arguments passed to
    createContext as the initial value. This is a hypothetical example,
    hence the shortcuts.
*/

const PoseContext = React.createContext('closed'); // is the drawer open or closed?
const CartContext = React.createContext([{
  ids: idsFromSomewhere,
  cartMap: objectFromSomewhereElse,
}]);

const App = () => (
  <PoseContext.Consumer>
    {pose => (
      <Drawer pose={pose}>
        <Title pose={pose}>Your Cart</Title>
        <CartContext.Consumer>
          {({ ids, cartMap }) => <List data={map(prop(__, cartMap), ids)} /> }
        </CartContext.Consumer>
      </Drawer>
    )}
  </PoseContext.Consumer>
);

render(<App />, document.getElementById('appRoot'));

Enter fullscreen mode Exit fullscreen mode

Well, that doesn't look very ugly now. But imagine if instead of using ramda and offloading to another component, we had something like this in the CartContext's Consumer:

<CartContext.Consumer>
  {({ ids, cartMap }) => (
    <Fragment>
      {ids.map((id) => {
        const product = cartMap[id];
        return (
          <CartItem onClick={clickHandler} key={id}>
            <Link route={`/products/${product.slug}/p/${product.id}`}>
              <a>{product.name}</a>
            </Link>
          </CartItem>
        );
      })}
    </Fragment>
  )}
</CartContext.Consumer>;
Enter fullscreen mode Exit fullscreen mode

Now imagine this, but with another Consumer called CouponConsumer to inject the app's Coupon related state. I would be terrified to look at Cart.js even if the culprit was me from 2 months ago. Enough banter, let's now be true to the title of this post and propose a solution to make neat code.

Adopting react-adopt (ok sorry no more)

The tiny library that saves the day.

GitHub logo pedronauck / react-adopt

😎 Compose render props components like a pro

😎 React Adopt - Compose render props components like a pro

GitHub release Build Status Codacy Badge

📜 Table of content

🧐   Why

Render Props are the new hype of React's ecosystem, that's a fact. So, when you need to use more than one render props component together, this can be boring and generate something called a "render props callback hell", like this:

Bad

💡   Solution

  • Small. 0.7kb minified!
  • Extremely Simple. Just a method!

React Adopt is a simple method that composes multiple render prop components, combining each prop result from your mapper.

📟   Demos

💻   Usage

Install as project dependency:

$ yarn add react-adopt
Enter fullscreen mode Exit fullscreen mode

Now you can use…

import { adopt } from 'react-adopt';

const CombinedContext = adopt({
  pose: <PoseContext.Consumer />,
  cart: <CartContext.Consumer />,
});

const App = () => (
  <CombinedContext>
    {({ pose, cart: { ids, cartMap } }) => (
      <Drawer pose={pose}>
        <Title pose={pose}>Your Cart</Title>
        <List data={map(prop(__, cartMap), ids)} />
      </Drawer>
    )}
  </CombinedContext>
);

Enter fullscreen mode Exit fullscreen mode

Neat, isn't it? We were able to compose two render prop components into a single one, and we could do the same thing with three or four. While Context Consumers are a great demo for this, we can utilise this neat trick for all render prop components and make our code more understandable and organised.

I'm trying to make it a habit to write every week, follow me if you think you want more of these tiny tricks that I picked up along my front end journey.

Latest comments (0)