skip to Main Content

The component listens to the context. Each context update triggers a rerender. Can I stop listening to the context if I already got the right value?
I want to stop further component update when this context changes

For example

const {contextValue, setContextValue}= useContext(MyContext)
if (contextValue == 'stop listen') {stop listening}

Is it possible?

2

Answers


  1. Consumers of useContext() will always re-render whenever the context value changes.

    "Even if an ancestor uses React.memo or shouldComponentUpdate, a rerender will still happen starting at the component itself using useContext."

    There’s a good chance that if you don’t always want context changes to trigger a component re-render, then some other means of maintaining that state is better for your use case.

    See the useContext React docs for more information.

    Login or Signup to reply.
  2. There is no way to wrangle a useContext invocation into a custom hook that wouldn’t cause a rerender when the context value changes. However we can achieve similar functionality in a higher order component.

    const withContextValue = Component => (props) => {
      const context = useContext(MyContext);
      const contextRef = useRef(context);
      if (contextRef.current.contextValue !== 'stop listen')
        contextRef.current = context;
      return <Component {...props} context={contextRef.current} />;  
    };
    

    Provided you pass a memoized component to withContextValue you’ll receive a component that will rerender on every context value change but the memoized component will not rerender unlike in the case where useContext was invoked inside the memoized component.


    The above pattern can be unwieldy to work with and doesn’t eliminate rerenders completely. For a better developer experience you may want to try a subscription pattern where each consumer of a context gets to define what changes cause a rerender. Famously react-redux uses this pattern.

    Define your context value as a mutable object.

    class ContextValue {
      constructor() {
        this.subscribers = [];
        this.contextValue = '';
      }
    
      setContextValue(action) {
        const prevValue = this.contextValue;
        this.contextValue =
          typeof action === 'function'
            ? action(this.contextValue)
            : action;
        if (prevValue !== this.contextValue)
          this.subscribers.forEach(subscriber => subscriber(this.contextValue));
      }
      
      subscribe(subscriber) {
        this.subscribers.push(subscriber);
        return () => void (this.subscribers = this.subscribers.filter(sub => sub !== subscriber));
      }
    }
    
    export const Provider = () => {
      const context = useRef(new ContextValue()).current;
      return <MyContext.Provider value={context}>{children}</MyContext.Provider>
    };
    
    export const useMyContext = (equalityFn = () => false) => {
      const [, rerender] = useReducer(() => ({}), {});
      const context = useContext(MyContext);
      const equalityFnRef = useRef(equalityFn);
      equalityFn.current = equalityFn;
      useEffect(
        () => context.subscribe(newValue => {
          if (!equalityFnRef.current(context.contextValue, newValue)) rerender();
        }),
        [context.subscriber, context.contextValue]
      );
      return context;
    };
    

    You can then define any equality function to limit rerenders on a consumer-by-consumer basis just like with Redux’s useSelector.

    const {contextValue, setContextValue}= useMyContext(prevValue => prevValue === 'stop listen');
    

    Mind you this pattern will stop unnecessary rerenders but will not stop the component from reading an up-to-date context value if something else causes a rerender. If you need to stop receiving new values you’ll want to wrap the hook in an extra layer of memoization.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search