loading...

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

blnkspace profile image Aviral Kulshreshtha Updated on ・3 min read

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'));

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>;

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

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>
);

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.

Posted on by:

blnkspace profile

Aviral Kulshreshtha

@blnkspace

Frontend engineer @Clarifai. Extremely appreciative of React and a functional and reactive approach to programming.

Discussion

markdown guide