react-tracked

repository·main·Indexed 25 days ago

https://github.com/dai-shi/react-tracked

A state usage tracking library using Proxies to optimize re-renders for useState, useReducer, React Redux, Zustand, and other state management tools. It ensures components only update when the specific properties accessed during render change. Key features include createContainer for custom state containers, createTrackedSelector for wrapping existing selector hooks, and getUntrackedObject to access raw objects without triggering subscriptions.

Tokens
11K
Snippets
41
Records
60
Agent score
80%

What's inside react-tracked

  1. Overview of React Tracked

    main
    React Tracked is a library designed to prevent unnecessary React re-renders by implementing "state usage tracking." Instead of re-rendering a component whenever any part of a state object changes (the default behavior of useContext), React Tracked uses Proxies to track exactly which properties are accessed during a render. A re-render is only triggered if one of the specifically accessed properties changes. This works for both top-level properties and deeply nested objects.
  2. Compare React Tracked with MobX

    main

    While React Tracked and MobX both utilize Proxies to provide effortless render optimization and similar ease of use, they differ fundamentally in state management philosophy:

    • MobX: Based on mutable states.
    • React Tracked: Based on immutable states (consistent with Pure React and React Redux).
  3. Compare React Tracked with Pure React Context and Hooks

    main
    The usage of createContainer in React Tracked is very similar to using pure React with context and hooks. The primary advantage is that React Tracked provides effortless render optimization via useTracked without the manual complexity often required in pure React implementations.
  4. Implement `useReducer` in `createContainer` via props

    main

    You can define a generic reducer and pass reducer, initialState, and init as props to the Provider. This is the most typical usage pattern for dynamic state initialization.

    const { 
      Provider, 
      useTracked, 
      // ... 
    } = createContainer(({ reducer, initialState, init }) => useReducer(reducer, initialState, init));
    
    const reducer = ...;
    
    const App = ({ initialState }) => (
      <Provider reducer={reducer} initialState={initialState}>
        ...
      </Provider>
    );
    const {
      Provider,
      useTracked,
      // ...
    } = createContainer(({ reducer, initialState, init }) => useReducer(reducer, initialState, init));
    
    const reducer = ...;
    
    const App = ({ initialState }) => (
      <Provider reducer={reducer} initialState={initialState}>
        ...
      </Provider>
    );
  5. Use createContainer with useState or useReducer

    main

    Use createContainer to turn a hook that returns state (like useState or useReducer) into a state-usage-tracking container. This prevents re-renders unless the specific properties accessed during render change.

    1. Define a custom hook that returns a state object (or a tuple [state, dispatch]).
    2. Call createContainer(yourHook) to get a Provider and a useTracked hook.
    3. Wrap your component tree with the returned Provider.
    4. Use useTracked() in components to access the proxied state.
    import { useState } from 'react';
    import { createContainer } from 'react-tracked';
    
    // 1. Define the hook
    const useValue = () =>
      useState({
        count: 0,
        text: 'hello',
      });
    
    // 2. Create the container
    const { Provider, useTracked } = createContainer(useValue);
    
    // 3. Use in a component
    const Counter = () => {
      const [state, setState] = useTracked();
      const increment = () => {
        setState((prev) => ({
          ...prev,
          count: prev.count + 1,
        }));
      };
      return (
        <div>
          <span >Count: {state.count}</span>
          <button type="button" onClick={increment}>
            +1
          </button>
        </div>
      );
    };
    
    // 4. Wrap App
    const App = () => (
      <Provider>
        <Counter />
      </Provider>
    );
  6. Sync container state with `propState`

    main

    If you have an existing state in props and want to sync it with a container, use useEffect inside useValue to update the container's state whenever the prop changes. Note that propState must be updated immutably.

    const useValue = ({ propState }) => {
      const [state, setState] = useState(propState);
      useEffect(() => {
        setState(propState);
      }, [propState]);
      return [state, setState];
    };
    
    const {
      Provider,
      useTracked,
      // ...
    } = createContainer(useValue);
    
    const App = ({ propState }) => <Provider propState={propState}>...</Provider>;
    const useValue = ({ propState }) => {
      const [state, setState] = useState(propState);
      useEffect(() => {
        // or useLayoutEffect
        setState(propState);
      }, [propState]);
      return [state, setState];
    };
    
    const {
      Provider,
      useTracked,
      // ...
    } = createContainer(useValue);
    
    const App = ({ propState }) => <Provider propState={propState}>...</Provider>;
  7. Implement `useState` in `createContainer` with an empty object

    main

    You can initialize state with an empty object. For TypeScript users, it is recommended to use a generic to define the state type.

    const {
      Provider,
      useTracked,
      // ...
    } = createContainer(() => useState<State>({}));
    
    const App = () => (
      <Provider>
        ...
      </Provider>
    );
    const {
      Provider,
      useTracked,
      // ...
    } = createContainer(() => useState<State>({}));
    
    
    const App = () => (
      <Provider>
        ...
      </Provider>
    );
  8. Dispatch actions via DOM event listeners

    main

    You can use useEffect within useValue to set up event listeners that call dispatch when events occur (e.g., window resizing).

    const useValue = () => {
      const [state, dispatch] = useReducer(reducer, initialState);
      useEffect(() => {
        const listener = () => {
          dispatch({
            type: 'WINDOW_RESIZED',
            width: window.innerWidth,
            height: window.innerHeight,
          });
        };
        window.addEventListener('resize', listener);
        return () => {
          window.removeEventListener('resize', listener);
        };
      }, []);
      return [state, dispatch];
    };
    
    const {
      Provider,
      useTracked,
      // ...
    } = createContainer(useValue);
    
    const App = () => (
      <Provider>
        ...
      </Provider>
    );
    const reducer = ...;
    const initialState = ...;
    
    const useValue = () => {
      const [state, dispatch] = useReducer(reducer, initialState);
      useEffect(() => {
        const listener = () => {
          dispatch({
            type: 'WINDOW_RESIZED',
            width: window.innerWidth,
            height: window.innerHeight,
          });
        };
        window.addEventListener('resize', listener);
        return () => {
          window.removeEventListener('resize', listener);
        };
      }, []);
      return [state, dispatch];
    };
    
    const {
      Provider,
      useTracked,
      // ...
    } = createContainer(useValue);
    
    const App = () => (
      <Provider>
        ...
      </Provider>
    );
  9. Create custom update functions in `createContainer`

    main

    To provide custom update functions (like increment or decrement) within your state, store them in the state object returned by useValue. You must use useCallback and useMemo to ensure the state object remains stable.

    Note: Using custom update functions prevents the benefits of concurrentMode in createContainer.

    const useValue = () => {
      const [count, setCount] = useState(0);
      const increment = useCallback(() => setCount((c) => c + 1), []);
      const decrement = useCallback(() => setCount((c) => c - 1), []);
      const state = useMemo(
        () => ({ count, increment, decrement }),
        [count, increment, decrement],
      );
      return [
        state,
        () => {
          throw new Error('use functions in the state');
        },
      ];
    };
    
    const { Provider, useTrackedState } = createContainer(useValue);
    
    const App = () => <Provider>...</Provider>;
    const useValue = () => {
      const [count, setCount] = useState(0);
      const increment = useCallback(() => setCount((c) => c + 1), []);
      const decrement = useCallback(() => setCount((c) => c - 1), []);
      const state = useMemo(
        () => ({
          count,
          increment,
          decrement,
        }),
        [count, increment, decrement],
      );
      return [
        state,
        () => {
          throw new Error('use functions in the state');
        },
      ];
    };
    
    const { Provider, useTrackedState } = createContainer(useValue);
    
    const App = () => <Provider>...</Provider>;
  10. Inspect tracked paths with AffectedDebugValue

    main

    In development mode, you can investigate the list of tracked paths for a specific state using useTrackedState.

    In React DevTools, navigate to the following path to see the AffectedDebugValue: TrackedState -> TrackedState -> AffectedDebugValue -> DebugValue.

  11. Use createTrackedSelector with React Redux

    main

    You can optimize useSelector from react-redux using createTrackedSelector. This allows components to only re-render when the specific properties they access from the Redux state change, without manually writing complex memoized selectors.

    import { useSelector, useDispatch } from 'react-redux';
    import { createTrackedSelector } from 'react-tracked';
    
    // Create the tracked version of useSelector
    const useTrackedSelector = createTrackedSelector(useSelector);
    
    const Counter = () => {
      // Use it just like useSelector, but with automatic property tracking
      const state = useTrackedSelector();
      const dispatch = useDispatch();
      
      return (
        <div >
          <span >Count: {state.count}</span>
          <button type="button" onClick={() => dispatch({ type: 'increment' })}>
            +1
          </button>
        </div>
      );
    };