Skip to content
This repository was archived by the owner on Nov 10, 2021. It is now read-only.

Commit a408d1e

Browse files
committed
useLayoutEffect to avoid unnecessary renders
1 parent 676a270 commit a408d1e

File tree

2 files changed

+52
-10
lines changed

2 files changed

+52
-10
lines changed

src/__tests__/index-test.tsx

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface IState {
1616
}
1717

1818
describe('redux-react-hook', () => {
19-
let subscriberCallback: (() => void) | null;
19+
let subscriberCallbacks: Array<() => void> = [];
2020
let state: IState;
2121
let cancelSubscription: () => void;
2222
let store: Store<IState, IAction>;
@@ -26,19 +26,23 @@ describe('redux-react-hook', () => {
2626
dispatch: jest.fn(action => action),
2727
getState: () => state,
2828
subscribe: jest.fn((l: () => void) => {
29-
subscriberCallback = l;
29+
subscriberCallbacks.push(l);
3030
return cancelSubscription;
3131
}),
3232
// tslint:disable-next-line:no-empty
3333
replaceReducer() {},
3434
});
3535

36-
function updateStore(newState: IState) {
36+
function updateStoreWithoutAct(newState: IState) {
3737
state = newState;
38+
for (const sub of subscriberCallbacks) {
39+
sub();
40+
}
41+
}
42+
43+
function updateStore(newState: IState) {
3844
act(() => {
39-
if (subscriberCallback) {
40-
subscriberCallback();
41-
}
45+
updateStoreWithoutAct(newState);
4246
});
4347
}
4448

@@ -53,7 +57,7 @@ describe('redux-react-hook', () => {
5357

5458
afterEach(() => {
5559
document.body.removeChild(reactRoot);
56-
subscriberCallback = null;
60+
subscriberCallbacks = [];
5761
});
5862

5963
function render(element: React.ReactElement<any>) {
@@ -265,6 +269,25 @@ describe('redux-react-hook', () => {
265269
expect(renderCount).toBe(2);
266270
});
267271

272+
it('renders once if have multiple useMappedState', () => {
273+
let renderCount = 0;
274+
const Component = ({prop}: {prop: any}) => {
275+
const mapState1 = React.useCallback((s: IState) => s.bar, [prop]);
276+
const mapState2 = React.useCallback((s: IState) => s.foo, [prop]);
277+
useMappedState(mapState1);
278+
useMappedState(mapState2);
279+
renderCount++;
280+
return null;
281+
};
282+
283+
render(<Component prop={1} />);
284+
285+
updateStoreWithoutAct({bar: 11, foo: '11'});
286+
updateStoreWithoutAct({bar: 12, foo: '12'});
287+
act(() => {});
288+
expect(renderCount).toBe(3);
289+
});
290+
268291
it('throws if provider is missing', () => {
269292
const Component = () => {
270293
const mapState = React.useCallback((s: IState) => s, []);

src/create.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@
33
import {
44
createContext,
55
useContext,
6-
useEffect,
76
useMemo,
87
useReducer,
98
useRef,
9+
useLayoutEffect,
10+
useEffect,
1011
} from 'react';
1112
import {Action, Dispatch, Store} from 'redux';
1213
import shallowEqual from './shallowEqual';
1314

15+
// React currently throws a warning when using useLayoutEffect on the server.
16+
// To get around it, we can conditionally useEffect on the server (no-op) and
17+
// useLayoutEffect in the browser.
18+
19+
const useIsomorphicLayoutEffect =
20+
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
21+
1422
class MissingProviderError extends Error {
1523
constructor() {
1624
super(
@@ -88,12 +96,23 @@ export function create<
8896

8997
const memoizedMapStateRef = useRef(memoizedMapState);
9098

91-
useEffect(() => {
99+
100+
// We use useLayoutEffect to render once if we have multiple useMappedState.
101+
// We need to update lastStateRef synchronously after rendering component,
102+
// With useEffect we would have:
103+
// 1) dispatch action
104+
// 2) call subscription cb in useMappedState1, call forceUpdate
105+
// 3) rerender component
106+
// 4) call useMappedState1 and useMappedState2 code
107+
// 5) calc new derivedState in useMappedState2, schedule updating lastStateRef, return new state, render component
108+
// 6) call subscription cb in useMappedState2, check if lastStateRef !== newDerivedState, call forceUpdate, rerender.
109+
// 7) update lastStateRef - it's too late, we already made one unnecessary render
110+
useIsomorphicLayoutEffect(() => {
92111
lastStateRef.current = derivedState;
93112
memoizedMapStateRef.current = memoizedMapState;
94113
});
95114

96-
useEffect(() => {
115+
useIsomorphicLayoutEffect(() => {
97116
let didUnsubscribe = false;
98117

99118
// Run the mapState callback and if the result has changed, make the

0 commit comments

Comments
 (0)