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 uses store-based object for context value,
while react-redux v6 used to use state-based object.
Using state-based object naively has
unable-to-bail-out issue,
but this library uses state-based object with
undocumented function calculateChangedBits
to stop propagation of re-renders.
See #29 for details.
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 {
Provider,
useDispatch,
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 = createStore(reducer);
const Counter = () => {
const state = useTrackedState();
const dispatch = useDispatch();
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();
const dispatch = useDispatch();
return (
<div>
{Math.random()}
<div>
<span>Text: {state.text}</span<
8000
span class="pl-c1">>
<input value={state.text} onChange={event => dispatch({ type: 'setText', text: event.target.value })} />
</div>
</div>
);
};
const App = () => (
<Provider store={store}>
<h1>Counter</h1>
<Counter />
<Counter />
<h1>TextBox</h1>
<TextBox />
<TextBox />
</Provider>
);
const store = createStore(...);
const App = () => (
<Provider store={store}>
...
</Provider>
);
const Component = () => {
const dispatch = useDispatch();
// ...
};
const Component = () => {
const selected = useSelector(selector);
// ...
};
const Component = () => {
const state = useTrackedState();
// ...
};
Experimental useTrackedSelectors
import React, { useCallback } from 'react';
import { useTrackedSelectors } from 'reactive-react-redux';
const globalSelectors = {
firstName: state => state.person.first,
lastName: state => state.person.last,
};
const Person = () => {
const { firstName } = useTrackedSelectors(globalSelectors);
return <div>{firstName}</div>;
// this component will only render when `state.person.first` is changed.
};
const Person2 = ({ threshold }) => {
const { firstName, isYoung } = useTrackedSelectors({
...globalSelectors,
isYoung: useCallback(state => (state.person.age < threshold), [threshold]),
});
return <div>{firstName}{isYoung && '(young)'}</div>;
};
The examples folder contains working examples. You can run one of them with
PORT=8080 npm run examples: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 10 11
See blog entry and #3 for details.
By relying on Proxy, there are some false negatives (failure to trigger re-renders) and some false positives (extra re-renders) in edge cases.
const state1 = useTrackedState();
const state2 = useTrackedState();
// state1 and state2 is not referentially equal
// even if the underlying redux state is referentially equal.
An object referential change doesn't trigger re-render if an property of the object is accessed in previous render
const state = useTrackedState();
const foo = useMemo(() => state.foo, [state]);
const bar = state.bar;
// if state.foo is not evaluated in render,
// it won't trigger re-render if only state.foo is changed.
const state = useTrackedState();
const dispatch = useDispatch();
dispatch({ type: 'FOO', value: state.foo }); // This may lead unexpected behavior if state.foo is an object
dispatch({ type: 'FOO', value: state.fooStr }); // This is OK if state.fooStr is a string
- A deadly simple React bindings library for Redux with Hooks API
- Developing React custom hooks for Redux without react-redux
- Integrating React and Redux, with Hooks and Proxies
- New React Redux coding style with hooks without selectors
- Benchmark alpha-released hooks API in React Redux with alternatives
- Four patterns for global state with React hooks: Context or Redux
- Redux meets hooks for non-redux users: a small concrete example with reactive-react-redux
- Redux-less context-based useSelector hook that has same performance as React-Redux
- What is state usage tracking? A novel approach to intuitive and performant global state with React hooks and Proxy