Convert Figma logo to code with AI

dai-shi logoreact-tracked

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

2,750
71
2,750
1

Top Related Projects

Official React bindings for Redux

Lightweight React bindings for MobX based on React 16.8 and Hooks

48,653

🐻 Bear necessities for state management in React

42,148

🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.

19,629

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:

  1. 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 }.

  1. 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.

  1. 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:

  1. Install the library using npm or yarn:
npm install react-tracked
  1. Create a tracked state in your application:
import { createTrackedState } from 'react-tracked';

const [useValue, setState] = createTrackedState({ count: 0 });
  1. Use the useValue hook to access the tracked state, and the setState 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>
  );
}
  1. 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.

48,653

🐻 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.

42,148

🤖 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.

19,629

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 Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

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.

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:

API

docs/api

Recipes

docs/recipes

Caveats

docs/caveats

Related projects

docs/comparison

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

NPM DownloadsLast 30 Days