DEV Community

Cover image for We fixed react context 🚀. Using selectors for granular reactivity
Abdullah Adeel
Abdullah Adeel

Posted on • Originally published at abdadeel.Medium

We fixed react context 🚀. Using selectors for granular reactivity

Using selectors with react context — textcon

Context in react was introduced to solve solely one problem. And that is prop drilling. But it has never been aimed to be used as a state manger. The primary reason is the render optimizations. Changes to the context value cause the whole child tree to re-render which is not ideal especially if state changes frequently and some of the child components are expensive to render. There are some workarounds but those too come with their own problems and limitations. State management libraries like redux has the concept of selectors from the beginning. Using selectors efficiently can increase the performance of react applications significantly.

In this article, I’ll introduce a library called textcon that I built on top of react context and support features like selectors and many more.

https://github.com/mabdullahadeel/textcon

If you want to see more in depth explanation and use of the library. You can watch this video.

A side note. Inspiration for this library came from this video of Jack Harrington. Highly recommend watching the video.

Installation

# using npm
npm install textcon

# using yarn
yarn add textcon

# using pnpm
pnpm add textcon
Enter fullscreen mode Exit fullscreen mode

Usage

Using textcon is very similar to plain react context with the following general steps.

  • Create context with default state and optionally actions.
  • Wrap component tree with the provider.
  • Use the provided hook to consume the state in components. In case of plain react context, this hook is useContext but textcon expose useStore with additional features. The main of which is the support of selectors.

let’s say we’re building a simple counter app. But instead of one counter, you’ve two counters that you need to keep track two counters. firstCounter and secondCounter. Keep that in mind, out default state would look something like this.

const defaultState = {
    firstCounter: 0,
    secondCounter: 0    
}
Enter fullscreen mode Exit fullscreen mode

Let’s use textcon to create context.

import { createContextStore } from 'textcon';

const {Provider, useStore} = createContextStore({
    firstCounter: 0,
    secondCounter: 0
})
Enter fullscreen mode Exit fullscreen mode

Simply import createContextStore from textcon and provide it the default state value.

This functional will return object. Let’s destruct it to use;

  • *************Provider*************: The component that needs to be wrapped around the component tree where you want to consume the state.
  • ***************useStore*************** hook: This hook is use to access the state stored in the context.

Now lets wrap our parent component (<App/ >) with the provider.

// ...
function App() {
  return (
    <Provider>
        <div className="App">
            Hello, World!
        </div>
    </Provider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now, let’s say we’ve two components.

  • A component to display the value of firstCounter
  • Another component to update the value of firstCounter
// ...
const Counter1Display = () => {
  const {get: firstCounter} = useStore((state) => state.firstCounter);

  return (
    <div>
      Counter 1: {firstCounter}
    </div>
  )
}

const Counter1Control = () => {
const { set } = useStore(() => false);

  return (
    <button onClick={() => {
                set((prev) => ({
                    ...prev,
                    firstCounter: prev.firstCounter + 1
                }))
            }}>
      Increment Counter 1
    </button>
  )
}
// ...
Enter fullscreen mode Exit fullscreen mode

In this snippet, we defined the two required components. Counter1Display component is responsible for rendering the current value of the firstCounter stored inside the context. Counter1Control component renders a button that when clicked increment the value of first counter by 1.

useStore hook works pretty similar to how useSelector hook works in redux. The first argument passed to this hooks is a selector function that can be used to select whole state (default) or part of the state the component is interested in.

In case of Counter1Display component, we’re only interested in firstCounter value since that’s the value this component is going to display.

// ...
const {get: firstCounter} = useStore((state) => state.firstCounter);
// ...
Enter fullscreen mode Exit fullscreen mode

Unlike redux’s useSelector hooks, useStore by textcon returns an object with a get property and set setter function property. get give access to the value returned by the selector provided to useSelector. While set as can use used just like useState hook from react to update the state stored in context.

One thing that you might’ve noticed is the callback function passed to useStore in Counter1Control component.

// ...
const { set } = useStore(() => false);
// ...
Enter fullscreen mode Exit fullscreen mode

Since Counter1Control component does not render any reactive state, a callback returning static value can be passed as selector to prevent re-renders.

Now, let’s render out counter components.

function App() {
  return (
    <Provider>
        <div className="App">
            <Counter1Display />
            <Counter1Control />
        </div>
    </Provider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

In the same way, state for second counter can be accessed and updated in the context.

Using actions

Actions are predefined functions to update the state object stored inside the context. Actions can be provided in an object as second argument to the createContextStore function.

Let’s add actions to update the counter values.

import { createContextStore, ActionablePayload } from 'textcon';

const {Provider, useStore, useActions} = createContextStore({
    firstCounter: 0,
    secondCounter: 0
},
{
    incrementFirstCounter: ({set, get}) => {
        set((prev) => ({
            ...prev,
            firstCounter: get().firstCounter + 1 // or prev.firstCounter + 1
        }))
    },
    decrementFirstCounter: ({set, get}) => {
        set((prev) => ({
            ...prev,
            firstCounter: get().firstCounter - 1 // or prev.firstCounter + 1
        }))
    },
    incrementBy: ({set, get}, action: ActionablePayload<number>) => {
        set((prev) => ({
            ...prev,
            firstCounter: get().firstCounter + action.payload
        }))
    }
})
Enter fullscreen mode Exit fullscreen mode

Actions can be triggered using useActions hook expose by the createContextStore function.

Let’s update our Counter1Control component to update use actions to update the state value store in context.

// ...
const Counter1Control = () => {
const { incrementFirstCounter } = useActions();

  return (
    <button onClick={incrementFirstCounter}>
      Increment Counter 1
    </button>
  )
}
// ...
Enter fullscreen mode Exit fullscreen mode

Much cleaner! In the same way,

// ...
const Counter1ControlByTen = () => {
const { incrementBy } = useActions();

  return (
    <button onClick={() => incrementBy(10)}>
      Increment Counter 1 by 10
    </button>
  )
}
// ...
Enter fullscreen mode Exit fullscreen mode

textcon comes with other useful features like global state persist and subscribing to state changes outside the react components.

https://github.com/mabdullahadeel/textcon

🚀

Top comments (0)