Skip to content

Commit 257e18c

Browse files
committed
Add lodash.isequal as a dependency. Clamp down on unnecessary re-renders!
1 parent b358cd5 commit 257e18c

File tree

4 files changed

+181
-121
lines changed

4 files changed

+181
-121
lines changed

README.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ Hooks implementation for Facebook [Flux Util](https://github.com/facebook/flux#F
33

44
This takes advantage of the new [React Hooks API](https://reactjs.org/docs/hooks-intro.html), and is a great alternative to using Flux-Util's [Container](https://facebook.github.io/flux/docs/flux-utils.html#container).
55

6-
This is a very simple implementation using a combination of [useEffect](https://reactjs.org/docs/hooks-reference.html#useeffect) & [useReducer](https://reactjs.org/docs/hooks-reference.html#usereducer).
6+
This is an implementation using a combination of [useEffect](https://reactjs.org/docs/hooks-reference.html#useeffect) & [useReducer](https://reactjs.org/docs/hooks-reference.html#usereducer) and [lodash.isequal](#strict-equality).
7+
78

89
## Install
910
```npm add flux-hooks``` or ```yarn add flux-hooks```
@@ -13,7 +14,7 @@ This is a very simple implementation using a combination of [useEffect](https://
1314

1415
const **value_from_store** = (**prevState**, **store**) => {...}
1516

16-
const **value** = **useFluxStore**(**store**: \<FluxStore>, **value_from_store**: Function, *deps*)
17+
const **value** = **useFluxStore**(**store**: \<FluxStore>, **value_from_store**: Function, **deps?**: Array, **strictEquality?**: boolean)
1718

1819
# Usage
1920

@@ -30,15 +31,19 @@ const CounterComponent = () => {
3031
}
3132
~~~
3233

33-
## Dependencies
34+
## Dependencies - **deps**
35+
36+
The **deps** parameter is an Array of values as used by useCallback/useMemo.
3437

35-
There are cases where the reducer is using other State/Prop values. Normally useReducer would not trigger a dispatch in this case. We use the **deps** parameter of useFluxStore as a list.
38+
In cases where the reducer is using other State/Prop, pass them as deps. Normally useReducer would not trigger a dispatch in this case.
3639

3740
~~~
3841
import useFluxStore from 'flux-hooks';
42+
3943
const SearchComponent = () => {
4044
const [query, setQuery] = useState("")
41-
const results = useFluxStore(SearchStore, (prevState, store) => store.getSearchResults(query))
45+
46+
const results = useFluxStore(SearchStore, (prevState, store) => store.getSearchResults(query), [query])
4247
4348
return <div>
4449
<input type="text" value={query} onChange={e => setQuery(e.target.value)} />
@@ -47,4 +52,15 @@ const SearchComponent = () => {
4752
</ul>
4853
</div>
4954
}
50-
~~~
55+
~~~
56+
57+
## Strict Equality
58+
### Default: **OFF**
59+
60+
Stores can update frequently with our reducer selecting only a small subset of the values. In the cases if you apply a filter on an [Immutable-js](https://immutable-js.github.io/immutable-js/docs) objects, or return multiple values using an Object, this will cause the [Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#Description) equality check to fail. This defeats the purpose of using the reducer!
61+
62+
To prevent this, **[lodash.isequal](https://www.npmjs.com/package/lodash.isequal) is used by default**. This does a deep check whenever the reducer is run, to make sure nothing has changed.
63+
64+
The assumption here is that the equality check is cheaper to run than a re-render.
65+
66+
To **opt out** of using the more expensive lodash.isequal check set **strictEquality** (4th argument) to **true**. This will return to useReducer's default behaviour.

index.js

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,56 @@
11
import {
2-
useEffect, useReducer, useMemo, useRef,
2+
useEffect, useReducer, useMemo,
33
} from 'react';
44

5-
// useFluxStore essentially combines useReducer and useEffect to use with FluxStores
5+
6+
import isEqual from 'lodash.isequal';
7+
8+
// useFluxStore combines useReducer and useEffect to use with FluxStores
69
// useReducer: Used to extract relevant values from the store
710
// useEffect is used to attach a listener to the store
811

9-
export function useFluxStore(store, reducer, deps = []) {
10-
// Use a ref to calculate reducer's initial value from current state of store.
11-
const initReducer = useRef();
1212

13-
if (!initReducer.current) {
14-
initReducer.current = reducer(null, store);
13+
export function useFluxStore(store, reducer, deps = [], strictEquality = false) {
14+
// We use Lodash's isEqual check to make sure the state hasn't changed
15+
// This can be expensive, but cheaper than a re-render
16+
function reducerWithEqualityCheck(_p, _store) {
17+
const refreshVal = reducer(_p, _store);
18+
19+
if (isEqual(refreshVal, _p)) return _p;
20+
return refreshVal;
1521
}
1622

17-
// We need to pass an initialArg otherwise the first *out* will be undefined
18-
const [out, _dispatch] = useReducer(reducer, initReducer.current);
23+
const [out, _dispatch] = useReducer(
24+
strictEquality ? reducer : reducerWithEqualityCheck,
25+
reducer(null, store),
26+
);
27+
28+
// Watch dependencies, and dispatch if they change
29+
useMemo(() => {
30+
_dispatch(store);
31+
}, deps); // eslint-disable-line react-hooks/exhaustive-deps
1932

20-
// For any dependencies in the reducer, we make sure to trigger the reducer again
21-
useMemo(() => _dispatch(store), deps); // eslint-disable-line react-hooks/exhaustive-deps
2233

2334
useEffect(() => {
35+
// Listener that is called when store is updated
2436
function listener() {
25-
// When store is updated, we dispatch an update to the reducer
37+
// We dispatch the store to the reducer
2638
_dispatch(store);
2739
}
2840

29-
// Attach reducer's listener to store
41+
// Attach listener to store
3042
const token = store.addListener(listener);
3143

32-
// Instead of setting initial value as the second parameter on the useReducer, dispatch here:
33-
// This avoids missing an update between useReducer --> render --> useEffect
44+
// This avoids potentially missing an update between useReducer --> render --> useEffect
3445
_dispatch(store);
3546

3647
// On useEffect destruction, remove the listener
37-
return () => token.remove();
48+
return token.remove;
3849
}, []); // eslint-disable-line react-hooks/exhaustive-deps
39-
// We make sure to pass [] so we're not attaching/detaching on every render
50+
// We make sure to pass [] so we're not attaching/detaching on every render
51+
4052
return out; // Reducer value gets returned to useFluxStore
4153
}
4254

55+
4356
export default useFluxStore;

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "flux-hooks",
3-
"version": "1.0.4",
3+
"version": "1.1.0",
44
"description": "Hooks implementation for Facebook Flux Util's Stores",
55
"main": "index.js",
66
"scripts": {
@@ -18,7 +18,9 @@
1818
},
1919
"keywords": [
2020
"react",
21-
"flux"
21+
"flux",
22+
"hooks",
23+
"hook"
2224
],
2325
"bugs": {
2426
"url": "https://github.com/Fieldscope/flux-hooks/issues"
@@ -30,5 +32,8 @@
3032
"eslint-plugin-import": "^2.17.2",
3133
"eslint-plugin-jsx-a11y": "^6.2.1",
3234
"eslint-plugin-react": "^7.13.0"
35+
},
36+
"dependencies": {
37+
"lodash.isequal": "^4.5.0"
3338
}
3439
}

0 commit comments

Comments
 (0)