DEV Community

Cover image for Creating Our Own Version of Context
Dowen Robinson
Dowen Robinson

Posted on

Creating Our Own Version of Context

If you're a React developer, you've probably heard of context. If you haven't, let me fill you in. Before I can explain, we'll need some (no pun intended) context. React allows you to share data between components via the use of props. This is great but we quickly run into problems when components that are deeply nested in our component tree require data that is also needed higher up in the tree. The most straight forward solution is to drill props or manually pass the data down the tree until it gets to where it is needed - yeaaaah not fun.

According to React's documentation, Context provides a way to pass data through the component tree without having to pass props down manually at every level ie. It allows us to skip drilling props like a crazy person. So how exactly does it work?
Let's have a look.

(This is the most contrived of examples)

1. Create Context

import { createContext } from "react";

export const MessageContext = createContext("");

Enter fullscreen mode Exit fullscreen mode

2. Wrap Section of Component Tree in Context.Provider

// index.js

import React from "react";
import { MessageContext } from "./context";
import ChildA from "./components/ChildA";
import ChildB from "./components/ChildB";

export default function App() {
  return (
    <MessageContext.Provider value="Message from context">
      <div style={{ fontFamily: "sans-serif", textAlign: "center" }}>
        <ChildA />
        <ChildB />
      </div>
    </MessageContext.Provider>
  );
}

Enter fullscreen mode Exit fullscreen mode

By wrapping this section of our component tree in the MessageContext.Provider tag, we're now able to access the value of the provider from any descendant components.

3. useContext

import React, { useContext } from "react";
import { MessageContext } from "../context";

function ChildA(props) {
  const message = useContext(MessageContext);
  return (
    <div>
      <h2>ChildA</h2>
      <p>{message}</p>
    </div>
  );
}

export default ChildA;

Enter fullscreen mode Exit fullscreen mode

Now you have an idea of how Context works, how about we create our own version.

Creating Our Own Context

First, let's create a component that will function as our Provider. Here are a few requirements I came up with for our Provider component:

  1. Accepts any data passed via props
  2. Can be overridden by descendant components
  3. Data will be passed to all descendant components

I'll post the completed code and then provide a walk through of exactly what's happening.

ancestor/index.js

function Ancestor(){

    function passProps(child) {

        if (Object.hasOwnProperty.call(child.props, 'children')) {
            const newChildren = Children.map(child.props.children, (_child) => {
                if (isValidElement(_child)) {
                    return passProps(_child)
                }
                return _child
            })

            return cloneElement(child, {
                ...props,
                ...child.props,
                children: newChildren,
            })
        }

        return cloneElement(child, { ...props, ...child.props })
    }


    const descendants = Children.map(children, (child) => passProps(child))


    return <>{descendants}</>
}
Enter fullscreen mode Exit fullscreen mode

To iterate through each child present in our children prop, we make use of React's Children property that exposes a map function similar to Array.map:

const descendants = Children.map(children, (child) => passProps(child))
Enter fullscreen mode Exit fullscreen mode

On each iteration, we pass an element to the passProps function. passProps is a recursive function that will iterate through every child and pass the props provided from our provider component.

passProps will first check if the child passed has a children property:

if (Object.hasOwnProperty.call(child.props, 'children'))
Enter fullscreen mode Exit fullscreen mode

If it does, it will iterate through each element checking if it's a valid React component. If it is valid, we return passProps with that element as an argument. Otherwise, we just return the element unchanged.

const newChildren = Children.map(child.props.children, (_child) => {
                if (isValidElement(_child)) {
                    return passProps(_child)
                }
                return _child
            }) 
Enter fullscreen mode Exit fullscreen mode

This creates new children. Our next step is to create a clone of the child passed to passProps and overwrite the children prop with our new children. React exposes a function called cloneElement. cloneElement functions similarly to Object.assign allowing us to create a clone of the element and assign new values and props.
This is also where we want to enforce requirement #2. By destructuring the child.props after the props object, we ensure that any child props override props passed from our provider component.

return cloneElement(child, {
                ...props,
                ...child.props,
                children: newChildren,
            })
Enter fullscreen mode Exit fullscreen mode

In the event that the child does not have children, we simply return a copy with our child props overriding the props from our provider component - similar to what we have above.

return cloneElement(child, { ...props, ...child.props })
Enter fullscreen mode Exit fullscreen mode

Now to get this all working, we simply need to return our new and improved children.

return <>{descendants}</>
Enter fullscreen mode Exit fullscreen mode

Let's try replacing the context provider from our example with our custom provider component but this time instead of a value, we'll pass a message prop.

App.js

import React from "react";
import Ancestor from "./ancestor"

export default function App() {
  return (
    <Ancestor message="Message from the ancestor">
      <div style={{ fontFamily: "sans-serif", textAlign: "center" }}>
        <ChildA />
        <ChildB />
      </div>
    </Ancestor>
  );
}

Enter fullscreen mode Exit fullscreen mode

How do we access the data passed down from Ancestor? Easy. We access it like any prop passed to a component.

ChildA.js

import React from "react";


function ChildA({message}) {

  return (
    <div>
      <h2>ChildA</h2>
      <p>{message}</p>
    </div>
  );
}

export default ChildA;
Enter fullscreen mode Exit fullscreen mode

Boom! We just made our own version of context. Albeit, a contrived version that has performance issues 😂.

You might be wondering why you'd ever need this. You probably don't. If you ever need to avoid drilling props, just useContext. If you prefer to live life on the edge, I made this code into a package. Give it a go npm i react-ancestor.

Thank you for reading! All questions and comments are appreciated 😄.

Follow me on Twitter @reactdon

Top comments (0)