React Tracked

State usage tracking with Proxies. Optimize re-renders for useState/useRedu...

README

logo

React Tracked

CI npm size discord

State usage tracking with Proxies. Optimize re-renders for useState/useReducer, React Redux, Zustand and others.

Documentation site: https://react-tracked.js.org

Introduction


Preventing re-renders is one of performance issues in React.
Smaller apps wouldn't usually suffer from such a performance issue,
but once apps have a central global state that would be used in
many components. The performance issue would become a problem.
For example, Redux is usually used for a single global state,
and React-Redux provides a selector interface to solve the performance issue.
Selectors are useful to structure state accessor,
however, using selectors only for performance wouldn't be the best fit.
Selectors for performance require understanding object reference
equality which is non-trival for beginners and
experts would still have difficulties for complex structures.

React Tracked is a library to provide so-called "state usage tracking."
It's a technique to track property access of a state object,
and only triggers re-renders if the accessed property is changed.
Technically, it uses Proxies underneath, and it works not only for
the root level of the object but also for deep nested objects.

Prior to v1.6.0, React Tracked is a library to replace React Context
use cases for global state. React hook useContext triggers re-renders
whenever a small part of state object is changed, and it would cause
performance issues pretty easily. React Tracked provides an API
that is very similar to useContext-style global state.

Since v1.6.0, it provides another building-block API
which is capable to create a "state usage tracking" hooks
from any selector interface hooks.
It can be used with React-Redux useSelector, and any other libraries
that provide useSelector-like hooks.

Install


This package requires some peer dependencies, which you need to install by yourself.

  1. ``` sh
  2. yarn add react-tracked react scheduler react-dom
  3. ```

For React Native users:

  1. ``` sh
  2. yarn add react-tracked react scheduler react-native
  3. ```

Usage


There are two main APIs createContainer and createTrackedSelector.
Both take a hook as an input and return a hook (or a container including a hook).

There could be various use cases. Here are some typical ones.

createContainer / useState


Define a useValue custom hook


  1. ``` js
  2. import { useState } from 'react';

  3. const useValue = () => useState({
  4.   count: 0,
  5.   text: 'hello',
  6. });
  7. ```

This can be useReducer or any hook that returns a tuple [state, dispatch].

Create a container


  1. ``` js
  2. import { createContainer } from 'react-tracked';

  3. const { Provider, useTracked } = createContainer(useValue);
  4. ```

useTracked in a component


  1. ``` js
  2. const Counter = () => {
  3.   const [state, setState] = useTracked();
  4.   const increment = () => {
  5.     setState((prev) => ({
  6.       ...prev,
  7.       count: prev.count + 1,
  8.     });
  9.   };
  10.   return (
  11.     <div>
  12.       <span>Count: {state.count}</span>
  13.       <button type="button" onClick={increment}>+1</button>
  14.     </div>
  15.   );
  16. };
  17. ```

The useTracked hook returns a tuple that useValue returns,
except that the first is the state wrapped by proxies and
the second part is a wrapped function for a reason.

Thanks to proxies, the property access in render is tracked and
this component will re-render only if state.count is changed.

Wrap your App with Provider


  1. ``` js
  2. const App = () => (
  3.   <Provider>
  4.     <Counter />
  5.     <TextBox />
  6.   </Provider>
  7. );
  8. ```

createTrackedSelector / react-redux


Create useTrackedSelector from useSelector


  1. ``` js
  2. import { useSelector, useDispatch } from 'react-redux';
  3. import { createTrackedSelector } from 'react-tracked';

  4. const useTrackedSelector = createTrackedSelector(useSelector);
  5. ```

useTrackedSelector in a component


  1. ``` js
  2. const Counter = () => {
  3.   const state = useTrackedSelector();
  4.   const dispatch = useDispatch();
  5.   return (
  6.     <div>
  7.       <span>Count: {state.count}</span>
  8.       <button type="button" onClick={() => dispatch({ type: 'increment' })}>+1</button>
  9.     </div>
  10.   );
  11. };
  12. ```

createTrackedSelector / zustand


Create useStore


  1. ``` js
  2. import create from 'zustand';

  3. const useStore = create(() => ({ count: 0 }));
  4. ```

Create useTrackedStore from useStore


  1. ``` js
  2. import { createTrackedSelector } from 'react-tracked';

  3. const useTrackedStore = createTrackedSelector(useStore);
  4. ```

useTrackedStore in a component


  1. ``` js
  2. const Counter = () => {
  3.   const state = useTrackedStore();
  4.   const increment = () => {
  5.     useStore.setState(prev => ({ count: prev.count + 1 }));
  6.   };
  7.   return (
  8.     <div>
  9.       <span>Count: {state.count}</span>
  10.       <button type="button" onClick={increment}>+1</button>
  11.     </div>
  12.   );
  13. };
  14. ```

API



Recipes



Caveats



Related projects




Examples


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

  1. ``` sh
  2. PORT=8080 yarn run examples:01_minimal
  3. ```

and open in your web browser.

You can also try them in codesandbox.io:

Benchmarks


See this for details.

Blogs