React useContextSelector hook in userland

use-context-selector

React Context and useContext is often used to avoid prop drilling, however it's known that there's a performance issue. When a context value is changed, all components that useContext will re-render.

useContextSelector is recently proposed. While waiting for the process, this library provides the API in userland.

Install

npm install use-context-selector

Usage

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

import { createContext, useContextSelector } from 'use-context-selector';

const context = createContext(null);

const Counter1 = () => {
  const count1 = useContextSelector(context, v => v[0].count1);
  const setState = useContextSelector(context, v => v[1]);
  const increment = () => setState(s => ({
    ...s,
    count1: s.count1 + 1,
  }));
  return (
    <div>
      <span>Count1: {count1}</span>
      <button type="button" onClick={increment}>+1</button>
      {Math.random()}
    </div>
  );
};

const Counter2 = () => {
  const count2 = useContextSelector(context, v => v[0].count2);
  const setState = useContextSelector(context, v => v[1]);
  const increment = () => setState(s => ({
    ...s,
    count2: s.count2 + 1,
  }));
  return (
    <div>
      <span>Count2: {count2}</span>
      <button type="button" onClick={increment}>+1</button>
      {Math.random()}
    </div>
  );
};

const StateProvider = ({ children }) => {
  const [state, setState] = useState({ count1: 0, count2: 0 });
  return (
    <context.Provider value={[state, setState]}>
      {children}
    </context.Provider>
  );
};

const App = () => (
  <StateProvider>
    <Counter1 />
    <Counter2 />
  </StateProvider>
);

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

Technical memo

React context by nature triggers propagation of component re-rendering
if a value is changed. To avoid this, this library uses undocumented
feature of calculateChangedBits. It then uses a subscription model
to force update when a component needs to re-render.

API

createContext

This creates a special context for useContextSelector.

Parameters

  • defaultValue any

Examples

const PersonContext = createContext({ firstName: '', familyName: '' });

Returns React.Context

useContextSelector

This hook returns context selected value by selector.
It will only accept context created by createContext.
It will trigger re-render if only the selected value is referencially changed.

Parameters

  • context React.Context
  • selector Function

Examples

const firstName = useContextSelector(PersonContext, state => state.firstName);

Returns any

useContext

This hook returns the entire context value.
Use this instead of React.useContext for consistent behavior.

Parameters

  • context React.Context

Examples

const person = useContext(PersonContext);

Returns any

Limitations

  • Subscriptions are per-context basis. So, even if there are multiple context providers in a component tree, all components are subscribed to all providers. This may lead false positives (extra re-renders).
  • In order to stop propagation, children of a context provider has to be either created outside of the provider or memoized with React.memo.
  • Context consumers are not supported.
  • The stale props issue can't be solved in userland. (workaround with try-catch)

Examples

The examples folder contains working examples.
You can run one of them with

PORT=8080 npm run examples:minimal

and open http://localhost:8080 in your web browser.

GitHub