Frontend Development 17 min read

React Context Re‑render Issues and an Optimized use‑context‑selector Implementation

This article explains why passing an object as a React Context Provider value can cause unnecessary re‑renders, introduces the community‑made use‑context‑selector library to mitigate the problem, analyzes its limitations, and presents a custom optimized implementation using useSyncExternalStore for precise component updates.

ByteFE
ByteFE
ByteFE
React Context Re‑render Issues and an Optimized use‑context‑selector Implementation

In a typical React application data is passed down via props, which becomes cumbersome for values needed by many components such as locale or UI theme. React Context allows sharing such values without prop‑drilling, but when the Provider's value is an object containing multiple fields, every change to any field forces all consumers to re‑render.

The article demonstrates this issue with a sample code where count1 , setCount1 , count2 and setCount2 are provided together. Updating count1 unintentionally re‑renders the component that only uses count2 , because the Provider creates a new object each render and React's shallow Object.is check sees a change.

import { createContext, useState, useContext } from "react";

const Context = createContext(null);

const StateProvider = ({ children }) => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
{children}
);
};

const Counter1 = () => {
  const { count1, setCount1 } = useContext(Context);
  return (
count1: {count1}
setCount1(n => n + 1)}>add count1
);
};

const Counter2 = () => {
  const { count2, setCount2 } = useContext(Context);
  return (
count2: {count2}
setCount2(n => n + 1)}>add count2
);
};

export default function App() {
  return (
);
}

To solve the problem, the community created use‑context‑selector which lets a component select only the pieces of context it actually uses, causing re‑renders only when those pieces change.

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

const Context = createContext(null);

const StateProvider = ({ children }) => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
{children}
);
};

const Counter1 = () => {
  const count1 = useContextSelector(Context, v => v.count1);
  const setCount1 = useContextSelector(Context, v => v.setCount1);
  return (
count1: {count1}
setCount1(n => n + 1)}>add count1
);
};

const Counter2 = () => {
  const count2 = useContextSelector(Context, v => v.count2);
  const setCount2 = useContextSelector(Context, v => v.setCount2);
  return (
count2: {count2}
setCount2(n => n + 1)}>add count2
);
};

While this reduces unnecessary renders, the article points out four remaining issues:

When a selector returns an object, shallow comparison still marks the component as changed.

Selectors cannot detect whether a selected field is actually used in the render output.

In React 17, using useReducer inside the library can cause both components to re‑render when only one state changes.

In React 18 the optimisation that skips renders when the reducer result is unchanged was removed, making the library behave like plain useContext .

To address these problems, the author builds a simplified version of use‑context‑selector that leverages useSyncExternalStore (or its polyfill use-sync-external-store ) and adds an optional equalityFn (defaulting to shallowEqual ) for custom comparison.

import { createContext as createContextOrig, useContext as useContextOrig, useLayoutEffect, useRef, useCallback, useSyncExternalStore } from "react";
import shallowEqual from "shallowequal";

const createProvider = ProviderOrig => {
  const ContextProvider = ({ value, children }) => {
    const contextValue = useRef();
    if (!contextValue.current) {
      const listeners = new Set();
      contextValue.current = { value, listeners };
    }
    useLayoutEffect(() => {
      contextValue.current.value = value;
      contextValue.current.listeners.forEach(listener => listener());
    }, [value]);
    return (
{children}
);
  };
  return ContextProvider;
};

function createContext(defaultValue) {
  const context = createContextOrig({ value: defaultValue, listeners: new Set() });
  context.Provider = createProvider(context.Provider);
  delete context.Consumer;
  return context;
}

function useContextSelector(context, selector, equalityFn = shallowEqual) {
  const contextValue = useContextOrig(context);
  const { value, listeners } = contextValue;

  const subscribe = useCallback(callback => {
    listeners.add(callback);
    return () => listeners.delete(callback);
  }, [listeners]);

  const lastSnapshot = useRef(selector(value));

  const getSnapshot = () => {
    const next = selector(contextValue.value);
    if (equalityFn(lastSnapshot.current, next)) {
      return lastSnapshot.current;
    }
    lastSnapshot.current = next;
    return next;
  };

  return useSyncExternalStore(subscribe, getSnapshot);
}

The new implementation keeps the Provider's reference stable, notifies listeners on value changes, and lets each consumer compute its own snapshot via the selector. By using shallowEqual as the default equality function, returning objects from selectors no longer forces re‑renders unless their contents actually differ.

Finally, the article shows a simple example of useSyncExternalStore to monitor the browser's online/offline status, illustrating how the hook works with a subscription function and a snapshot getter.

import { useSyncExternalStore } from 'react';

export default function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  return
{isOnline ? '✅ Online' : '❌ Disconnected'}
;
}

function getSnapshot() {
  return navigator.onLine;
}

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

References to the original library, React documentation, and related articles are listed at the end of the article.

frontendPerformancereactHooksContextuseContextSelector
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.