react-tracked
State usage tracking with Proxies. Optimize re-renders for useState/useReducer, React Redux, Zustand and others.
Top Related Projects
Official React bindings for Redux
Lightweight React bindings for MobX based on React 16.8 and Hooks
🐻 Bear necessities for state management in React
🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.
Recoil is an experimental state management library for React apps. It provides several capabilities that are difficult to achieve with React alone, while being compatible with the newest features of React.
Quick Overview
react-tracked
is a state management library for React that provides a simple and efficient way to manage global state in your React applications. It uses a context-based approach to share state across components, allowing for easy access and updates to the state.
Pros
- Simplicity:
react-tracked
has a straightforward API and a small footprint, making it easy to integrate into existing projects. - Performance: The library uses memoization and selective re-rendering to optimize performance, ensuring that only the necessary components are re-rendered when the state changes.
- Flexibility:
react-tracked
can be used with both class-based and functional components, and it supports TypeScript out of the box. - Testability: The library's design makes it easy to test your components and state management logic.
Cons
- Limited Ecosystem: Compared to larger state management libraries like Redux or MobX,
react-tracked
has a smaller ecosystem of third-party packages and tooling. - Learning Curve: While the API is simple, developers who are new to context-based state management may need to invest some time to understand the library's concepts and best practices.
- Lack of Middleware:
react-tracked
does not provide built-in support for middleware, which can be useful for implementing features like logging, error handling, or async actions. - Dependency on React Context: The library's reliance on React Context means that it may not be suitable for use in older versions of React that do not support the Context API.
Code Examples
Here are a few examples of how to use react-tracked
:
- Creating a Tracked State:
import { createTrackedState } from 'react-tracked';
const [useValue, setState] = createTrackedState({ count: 0 });
This creates a tracked state with an initial value of { count: 0 }
.
- Accessing and Updating the Tracked State:
function Counter() {
const count = useValue((state) => state.count);
const increment = () => setState((state) => ({ count: state.count + 1 }));
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
This component uses the useValue
hook to access the count
value from the tracked state, and the setState
function to update the state.
- Nested Tracked State:
const [useNestedValue, setNestedState] = createTrackedState({
user: {
name: 'John Doe',
age: 30,
},
});
function UserProfile() {
const { name, age } = useNestedValue((state) => state.user);
return (
<div>
<p>Name: {name}</p>
<p>Age: {age}</p>
</div>
);
}
This example demonstrates how to work with nested tracked state.
Getting Started
To get started with react-tracked
, follow these steps:
- Install the library using npm or yarn:
npm install react-tracked
- Create a tracked state in your application:
import { createTrackedState } from 'react-tracked';
const [useValue, setState] = createTrackedState({ count: 0 });
- Use the
useValue
hook to access the tracked state, and thesetState
function to update it:
function Counter() {
const count = useValue((state) => state.count);
const increment = () => setState((state) => ({ count: state.count + 1 }));
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
- Wrap your application with the
Provider
component to make the tracked state available to all components:
import { Provider } from 'react-tracked';
ReactDOM.render(
<Provider>
<App />
</Provider>,
document.getElementById('root')
);
Competitor Comparisons
Official React bindings for Redux
Pros of react-redux
- Well-established ecosystem with extensive documentation and community support
- Powerful DevTools for debugging and time-travel debugging
- Middleware support for handling side effects and async operations
Cons of react-redux
- Steeper learning curve due to additional concepts like actions and reducers
- More boilerplate code required for setup and state management
- Can lead to over-engineering for simpler applications
Code Comparison
react-redux:
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return (
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
Count: {count}
</button>
);
}
react-tracked:
import { useTracked } from 'react-tracked';
function Counter() {
const [state, setState] = useTracked();
return (
<button onClick={() => setState(s => ({ ...s, count: s.count + 1 }))}>
Count: {state.count}
</button>
);
}
react-tracked offers a simpler API with less boilerplate, making it easier to use for smaller projects. However, react-redux provides more robust tools for complex state management in larger applications. The choice between the two depends on the project's scale and requirements.
Lightweight React bindings for MobX based on React 16.8 and Hooks
Pros of mobx-react-lite
- More mature and widely adopted ecosystem
- Better performance for large-scale applications
- Seamless integration with existing MobX projects
Cons of mobx-react-lite
- Steeper learning curve for developers new to MobX
- Requires more boilerplate code for setup and configuration
- Less flexible for fine-grained optimizations
Code Comparison
react-tracked:
import { Provider, useTracked } from 'react-tracked';
const [state, setState] = useTracked();
setState(prev => ({ ...prev, count: prev.count + 1 }));
mobx-react-lite:
import { observer } from 'mobx-react-lite';
import { makeAutoObservable } from 'mobx';
class Store {
count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count++;
}
}
const MyComponent = observer(({ store }) => (
<button onClick={() => store.increment()}>{store.count}</button>
));
Summary
react-tracked offers a simpler API and easier setup for React projects, making it ideal for smaller to medium-sized applications. It provides fine-grained reactivity without the need for decorators or complex store configurations.
mobx-react-lite, on the other hand, is better suited for larger applications with complex state management needs. It offers robust performance optimizations and seamless integration with the broader MobX ecosystem, but comes with a steeper learning curve and more verbose setup process.
🐻 Bear necessities for state management in React
Pros of zustand
- Simpler API with less boilerplate code
- Better TypeScript support out of the box
- More flexible, allowing for middleware and custom store enhancers
Cons of zustand
- Lacks automatic memoization and re-render optimization
- May require more manual performance tuning for complex state structures
- Doesn't provide built-in context-based state isolation
Code Comparison
react-tracked:
const useValue = () => useState({ count: 0 });
const { Provider, useTracked } = createContainer(useValue);
const Counter = () => {
const [state, setState] = useTracked();
return <button onClick={() => setState(s => ({ ...s, count: s.count + 1 }))}>
{state.count}
</button>;
};
zustand:
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}));
const Counter = () => {
const count = useStore(state => state.count);
const increment = useStore(state => state.increment);
return <button onClick={increment}>{count}</button>;
};
Both libraries aim to simplify state management in React applications, but they take different approaches. react-tracked focuses on optimizing re-renders and providing a more React-like API, while zustand offers a more flexible and lightweight solution with a focus on simplicity and ease of use.
🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.
Pros of Query
- Robust caching and synchronization features for remote data fetching
- Built-in support for pagination, infinite scrolling, and optimistic updates
- Large ecosystem with adapters for various frameworks and libraries
Cons of Query
- Steeper learning curve due to more complex API and concepts
- Potentially overkill for simple state management needs
- Larger bundle size compared to lightweight solutions
Code Comparison
React Tracked:
import { createContainer } from 'react-tracked';
const useValue = () => useState({ count: 0 });
const { Provider, useTracked } = createContainer(useValue);
function Counter() {
const [state, setState] = useTracked();
return <button onClick={() => setState(s => ({ ...s, count: s.count + 1 }))}>
{state.count}
</button>;
}
Query:
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function Counter() {
const { data, refetch } = useQuery(['counter'], () => fetchCount());
return <button onClick={() => refetch()}>
{data?.count ?? 'Loading...'}
</button>;
}
React Tracked focuses on simple state management with minimal boilerplate, while Query excels in handling complex data fetching scenarios with advanced caching and synchronization features.
Recoil is an experimental state management library for React apps. It provides several capabilities that are difficult to achieve with React alone, while being compatible with the newest features of React.
Pros of Recoil
- Developed and maintained by Facebook, ensuring long-term support and updates
- Offers advanced features like atom effects and selectors for complex state management
- Provides built-in performance optimizations and time-travel debugging capabilities
Cons of Recoil
- Steeper learning curve due to its unique concepts and API
- Requires wrapping the entire app with RecoilRoot, which may not be ideal for gradual adoption
- Larger bundle size compared to React Tracked
Code Comparison
Recoil:
import { atom, useRecoilState } from 'recoil';
const counterState = atom({
key: 'counterState',
default: 0,
});
function Counter() {
const [count, setCount] = useRecoilState(counterState);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
React Tracked:
import { createContainer } from 'react-tracked';
const useValue = () => useState({ count: 0 });
const { Provider, useTracked } = createContainer(useValue);
function Counter() {
const [state, setState] = useTracked();
return <button onClick={() => setState({ count: state.count + 1 })}>{state.count}</button>;
}
Both libraries offer efficient state management solutions for React applications, with Recoil providing more advanced features at the cost of complexity, while React Tracked focuses on simplicity and ease of use.
Convert designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual CopilotREADME
React Tracked
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.
npm add react-tracked react scheduler
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
import { useState } from 'react';
const useValue = () =>
useState({
count: 0,
text: 'hello',
});
This can be useReducer or any hook that returns a tuple [state, dispatch]
.
Create a container
import { createContainer } from 'react-tracked';
const { Provider, useTracked } = createContainer(useValue);
useTracked 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>
);
};
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
const App = () => (
<Provider>
<Counter />
<TextBox />
</Provider>
);
createTrackedSelector / react-redux
Create useTrackedSelector
from useSelector
import { useSelector, useDispatch } from 'react-redux';
import { createTrackedSelector } from 'react-tracked';
const useTrackedSelector = createTrackedSelector(useSelector);
useTrackedSelector in a component
const Counter = () => {
const state = useTrackedSelector();
const dispatch = useDispatch();
return (
<div>
<span>Count: {state.count}</span>
<button type="button" onClick={() => dispatch({ type: 'increment' })}>
+1
</button>
</div>
);
};
createTrackedSelector / zustand
Create useStore
import create from 'zustand';
const useStore = create(() => ({ count: 0 }));
Create useTrackedStore
from useStore
import { createTrackedSelector } from 'react-tracked';
const useTrackedStore = createTrackedSelector(useStore);
useTrackedStore in a component
const Counter = () => {
const state = useTrackedStore();
const increment = () => {
useStore.setState((prev) => ({ count: prev.count + 1 }));
};
return (
<div>
<span>Count: {state.count}</span>
<button type="button" onClick={increment}>
+1
</button>
</div>
);
};
Notes with React 18
This library internally uses use-context-selector
,
a userland solution for useContextSelector
hook.
React 18 changes useReducer behavior which use-context-selector
depends on.
This may cause an unexpected behavior for developers.
If you see more console.log
logs than expected,
you may want to try putting console.log
in useEffect.
If that shows logs as expected, it's an expected behavior.
For more information:
- https://github.com/dai-shi/use-context-selector/issues/100
- https://github.com/dai-shi/react-tracked/issues/177
API
Recipes
Caveats
Related projects
https://github.com/dai-shi/lets-compare-global-state-with-react-hooks
Examples
The examples folder contains working examples. You can run one of them with
PORT=8080 pnpm run examples:01_minimal
and open http://localhost:8080 in your web browser.
You can also try them directly: 01 02 03 04 05 06 07 08 09 10 11 12 13
Benchmarks
See this for details.
Blogs
- Super performant global state with React context and hooks
- Redux-less context-based useSelector hook that has same performance as React-Redux
- Four different approaches to non-Redux global state libraries
- What is state usage tracking? A novel approach to intuitive and performant global state with React hooks and Proxy
- How to use react-tracked: React hooks-oriented Todo List example
- Effortless render optimization with state usage tracking with React hooks
- 4 options to prevent extra rerenders with React context
- React Tracked Documentation Website with Docusaurus v2
Top Related Projects
Official React bindings for Redux
Lightweight React bindings for MobX based on React 16.8 and Hooks
🐻 Bear necessities for state management in React
🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.
Recoil is an experimental state management library for React apps. It provides several capabilities that are difficult to achieve with React alone, while being compatible with the newest features of React.
Convert designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual Copilot