Skip to content
React Redux binding with React Hooks and Proxy
JavaScript
Branch: master
Clone or download
Latest commit 7e37ecd Aug 13, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
__tests__ remove experimental hook Jul 24, 2019
dist run compile Jul 27, 2019
examples minor change Aug 12, 2019
src remove experimental hook Jul 24, 2019
.eslintrc.json obsolete ConcurrentMode May 30, 2019
.gitignore initial commit Nov 15, 2018
.travis.yml initial commit Nov 15, 2018
CHANGELOG.md v4.2.0 Jul 27, 2019
LICENSE v0.9.0; useReduxStateSimple Jan 10, 2019
README.md add blog item Aug 13, 2019
package-lock.json
package.json async example Aug 11, 2019
tsconfig.json rename-project Mar 31, 2019
webpack.config.js rename-project Mar 31, 2019

README.md

reactive-react-redux

Build Status npm version bundle size

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.

Introduction

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.

1. useTrackedState hook

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.

2. state-based object for context value

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.

How tracking works

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.

Install

npm install reactive-react-redux

Usage (useTrackedState)

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>
        <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>
);

API

Provider

const store = createStore(...);
const App = () => (
  <Provider store={store}>
    ...
  </Provider>
);

useDispatch

const Component = () => {
  const dispatch = useDispatch();
  // ...
};

useSelector

const Component = () => {
  const selected = useSelector(selector);
  // ...
};

useTrackedState

const Component = () => {
  const state = useTrackedState();
  // ...
};

Examples

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 11 12

Benchmarks

benchmark result

See #32 for details.

Limitations in tracking

By relying on Proxy, there are some false negatives (failure to trigger re-renders) and some false positives (extra re-renders) in edge cases.

Proxied states are referentially equal only in per-hook basis

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.

Proxied state shouldn't be used outside of render

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

Blogs

You can’t perform that action at this time.