This project is no longer maintained. react-tracked works with react-redux and covers the use case of reactive-react-redux. Redux docs officially recommends proxy-memoize as a selector library, and it provides similar developer experience to that of reactive-react-redux. Both are good options.
There are several projects related to this repo. Here's the index of those.
React Redux binding with React Hooks and Proxy
If you are looking for a non-Redux library, please visit react-tracked which has the same hooks API.
This is a library to bind React and Redux with Hooks API. It has mostly the same API as the official react-redux Hooks API, so it can be used as a drop-in replacement if you are using only basic functionality.
There are two major features in this library that are not in the official react-redux.
This library provides another hook useTrackedState
which is a simpler API than already simple useSelector
.
It returns an entire state, but the library takes care of
optimization of re-renders.
Most likely, useTrackedState
performs better than
useSelector
without perfectly tuned selectors.
Technically, useTrackedState
has no stale props issue.
react-redux v7 has APIs around Context.
This library is implemented with useMutableSource,
and it patches the Redux store.
APIs are provided without Context.
It's up to developers to use Context based on them.
Check out ./examples/11_todolist/src/context.ts
.
There's another difference from react-redux v7. This library directly use useMutableSource, and requires useCallback for the selector in useSelector. equalityFn is not supported.
A hook useTrackedState
returns an entire Redux state object with Proxy,
and it keeps track of which properties of the object are used
in render. When the state is updated, this hook checks
whether used properties are changed.
Only if it detects changes in the state,
it triggers a component to re-render.
npm install reactive-react-redux
import React from 'react';
import { createStore } from 'redux';
import {
patchStore,
useTrackedState,
} from 'reactive-react-redux';
const initialState = {
count: 0,
text: 'hello',
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'increment': return { ...state, count: state.count + 1 };
case 'decrement': return { ...state, count: state.count - 1 };
case 'setText': return { ...state, text: action.text };
default: return state;
}
};
const store = patchStore(createStore(reducer));
const Counter = () => {
const state = useTrackedState(store);
const { dispatch } = store;
return (
<div>
{Math.random()}
<div>
<span>Count: {state.count}</span>
<button type="button" onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button type="button" onClick={() => dispatch({ type: 'decrement' })}>-1</button>
</div>
</div>
);
};
const TextBox = () => {
const state = useTrackedState(store);
const { dispatch } = store;
return (
<div>
{Math.random()}
<div>
<span>Text: {state.text}</span>
<input value={state.text} onChange={event => dispatch({ type: 'setText', text: event.target.value })} />
</div>
</div>
);
};
const App = () => (
<>
<h1>Counter</h1>
<Counter />
<Counter />
<h1>TextBox</h1>
<TextBox />
<TextBox />
</>
);
patch Redux store for React
store
Store<State, Action> import { createStore } from 'redux';
import { patchStore } from 'reactive-react-redux';
const reducer = ...;
const store = patchStore(createStore(reducer));
useTrackedState hook
It return the Redux state wrapped by Proxy, and the state prperty access is tracked. It will only re-render if accessed properties are changed.
patchedStore
PatchedStore<State, Action> opts
Opts (optional, default {}
)import { useTrackedState } from 'reactive-react-redux';
const Component = () => {
const state = useTrackedState(store);
...
};
useSelector hook
selector has to be stable. Either define it outside render or use useCallback if selector uses props.
patchedStore
PatchedStore<State, Action> selector
function (state: State): Selected import { useCallback } from 'react';
import { useSelector } from 'reactive-react-redux';
const Component = ({ count }) => {
const isBigger = useSelector(store, useCallack(state => state.count > count, [count]));
...
};
memo
Using React.memo
with tracked state is not compatible,
because React.memo
stops state access, thus no tracking occurs.
This is a special memo to be used instead of React.memo
with tracking support.
Component
any areEqual
any? import { memo } from 'reactive-react-redux';
const ChildComponent = memo(({ obj1, obj2 }) => {
// ...
});
You can create Context based APIs like react-redux v7.
import { createContext, createElement, useContext } from 'react';
import {
PatchedStore,
useSelector as useSelectorOrig,
useTrackedState as useTrackedStateOrig,
} from 'reactive-react-redux';
export type State = ...;
export type Action = ...;
const Context = createContext(new Proxy({}, {
get() { throw new Error('use Provider'); },
}) as PatchedStore<State, Action>);
export const Provider: React.FC<{ store: PatchedStore<State, Action> }> = ({
store,
children,
}) => createElement(Context.Provider, { value: store }, children);
export const useDispatch = () => useContext(Context).dispatch;
export const useSelector = <Selected>(
selector: (state: State) => Selected,
) => useSelectorOrig(useContext(Context), selector);
export const useTrackedState = () => useTrackedStateOrig(useContext(Context));
You can create a selector hook with tracking support.
import { useTrackedState } from 'reactive-react-redux';
export const useTrackedSelector = (patchedStore, selector) => selector(useTrackedState(patchedStore));
Please refer this issue for more information.
You can combine useTrackedState and useDispatch to
make a hook that returns a tuple like useReducer
.
import { useTrackedState, useDispatch } from 'reactive-react-redux';
export const useTracked = (patchedStore) => {
const state = useTrackedState(patchedStore);
const dispatch = useDispatch(patchedStore);
return useMemo(() => [state, dispatch], [state, dispatch]);
};
Proxy and state usage tracking may not work 100% as expected. There are some limitations and workarounds.
const state1 = useTrackedState(patchedStore);
const state2 = useTrackedState(patchedStore);
// state1 and state2 is not referentially equal
// even if the underlying redux state is referentially equal.
You should use useTrackedState
only once in a component.
const state = useTrackedState(patchedStore);
const { foo } = state;
return <Child key={foo.id} foo={foo} />;
const Child = React.memo(({ foo }) => {
// ...
};
// if foo doesn't change, Child won't render, so foo.id is only marked as used.
// it won't trigger Child to re-render even if foo is changed.
You need to use a special memo
provided by this library.
import { memo } from 'reactive-react-redux';
const Child = memo(({ foo }) => {
// ...
};
Proxies are basically transparent, and it should behave like normal objects. However, there can be edge cases where it behaves unexpectedly. For example, if you console.log a proxied value, it will display a proxy wrapping an object. Notice, it will be kept tracking outside render, so any prorerty access will mark as used to trigger re-render on updates.
useTrackedState will unwrap a Proxy before wrapping with a new Proxy, hence, it will work fine in usual use cases. There's only one known pitfall: If you wrap proxied state with your own Proxy outside the control of useTrackedState, it might lead memory leaks, because useTrackedState wouldn't know how to unwrap your own Proxy.
To work around such edge cases, the first option is to use primitive values.
const state = useTrackedState(patchedStore);
const dispatch = useUpdate(patchedStore);
dispatch({ type: 'FOO', value: state.fooObj }); // Instead of using objects,
dispatch({ type: 'FOO', value: state.fooStr }); // Use primitives.
The second option is to use getUntrackedObject
.
import { getUntrackedObject } from 'react-tracked';
dispatch({ type: 'FOO', value: getUntrackedObject(state.fooObj) });
You could implement a special dispatch function to do this automatically.
The examples folder contains working examples. You can run one of them with
PORT=8080 npm run examples:01_minimal
and open http://localhost:8080 in your web browser.
You can also try them in codesandbox.io: 01 02 03 04 05 06 07 08 09 11 12 13
See #32 for details.