JSPM

@fluentui/react-context-selector

0.0.0-nightly-20250305-0406.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 422186
  • Score
    100M100P100Q180289F
  • License MIT

React useContextSelector hook in userland

Package Exports

    Readme

    @fluentui/react-context-selector

    React useContextSelector() hook in userland.

    Introduction

    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 are subscribed with useContext() will re-render.

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

    Installation

    NPM

    npm install --save @fluentui/react-context-selector

    Yarn

    yarn add @fluentui/react-context-selector

    Usage

    Getting started

    import * as React from 'react';
    import { createContext, useContextSelector, ContextSelector } from '@fluentui/react-context-selector';
    
    interface CounterContextValue {
      count1: number;
      count2: number;
      incrementCount1: () => void;
      incrementCount2: () => void;
    }
    
    // 💡 The same syntax as native React context API
    //    https://reactjs.org/docs/context.html#reactcreatecontext
    const CounterContext = createContext<CounterContextValue>({} as CounterContextValue);
    
    const CounterProvider = CounterContext.Provider;
    
    // not necessary but can be a good layer to mock for unit testing
    const useCounterContext = <T,>(selector: ContextSelector<CounterContextValue, T>) =>
      useContextSelector(CounterContext, selector);
    
    const Counter1 = () => {
      // 💡 Context updates will be propagated only when result of a selector function will change
      //    "Object.is()" is used for internal comparisons
      const count1 = useCounterContext(context => context.count1);
      const increment = useCounterContext(context => context.incrementCount1);
    
      return <button onClick={increment}>Counter 1: {count1}</button>;
    };
    
    const Counter2 = () => {
      const count2 = useCounterContext(context => context.count2);
      const increment = useCounterContext(context => context.incrementCount2);
    
      return <button onClick={increment}>Counter 2: {count2}</button>;
    };
    
    export default function App() {
      const [state, setState] = React.useState({ count1: 0, count2: 0 });
    
      const incrementCount1 = React.useCallback(() => setState(s => ({ ...s, count1: s.count1 + 1 })), [setState]);
      const incrementCount2 = React.useCallback(() => setState(s => ({ ...s, count2: s.count2 + 1 })), [setState]);
    
      return (
        <div className="App">
          <CounterProvider
            value={{
              count1: state.count1,
              count2: state.count2,
              incrementCount1,
              incrementCount2,
            }}
          >
            <Counter1 />
            <Counter2 />
          </CounterProvider>
        </div>
      );
    }

    useHasParentContext

    This helper hook will allow you to know if a component is wrapped by a context selector provider

    const Foo = () => {
      // An easy way to test if a context provider is wrapped around this component
      // since it's more complicated to compare with a default context value
      const isWrappedWithContext = useHasParentContext(CounterContext);
    
      if (isWrappedWithContext) {
        return <div>I am inside context selector provider</div>;
      } else {
        return <div>I can only use default context value</div>;
      }
    };

    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.

    Limitations

    • In order to stop propagation, children of a context provider has to be either created outside of the provider or memoized with React.memo.
    • <Consumer /> components are not supported.
    • The stale props issue can't be solved in userland. (workaround with try-catch)

    The implementation is heavily inspired by: