-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add: Add a useShallowEqualSelector hook
The useShallowEqualSelector hooks allows to avoid re-renders if an object is selected from the redux store but its value(s) didn't change. With the standard selector which uses `===` comparison even updating an object's value to the same value will cause a re-render (because a new state object is created). This pattern can be found at https://react-redux.js.org/api/hooks#recipe-useshallowequalselector
- Loading branch information
1 parent
ce1c182
commit a6d9b92
Showing
2 changed files
with
116 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/* SPDX-FileCopyrightText: 2024 Greenbone AG | ||
* | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
/* eslint-disable react/prop-types */ | ||
|
||
import {useCallback} from 'react'; | ||
import {useSelector, useDispatch} from 'react-redux'; | ||
import {configureStore} from '@reduxjs/toolkit'; | ||
|
||
import {describe, test, expect, testing} from '@gsa/testing'; | ||
|
||
import {fireEvent, rendererWith, screen} from 'web/utils/testing'; | ||
|
||
import useShallowEqualSelector from '../useShallowEqualSelector'; | ||
|
||
const reducer = (state = {value: 0}, action) => { | ||
switch (action.type) { | ||
case 'increment': | ||
return {...state, value: 1}; | ||
default: | ||
return state; | ||
} | ||
}; | ||
|
||
const update = () => ({type: 'increment'}); | ||
|
||
const TestComponent1 = ({renderCallback}) => { | ||
const state = useSelector(state => state.counter); | ||
const dispatch = useDispatch(); | ||
const updateCounter = useCallback(() => dispatch(update()), [dispatch]); | ||
renderCallback(); | ||
return ( | ||
<div> | ||
<div data-testid="counter">{state.value}</div> | ||
<button data-testid="update" onClick={updateCounter}> | ||
Increment | ||
</button> | ||
</div> | ||
); | ||
}; | ||
|
||
const TestComponent2 = ({renderCallback}) => { | ||
const state = useShallowEqualSelector(state => state.counter); | ||
renderCallback(); | ||
return ( | ||
<div> | ||
<div data-testid="shallowCounter">{state.value}</div> | ||
</div> | ||
); | ||
}; | ||
|
||
describe('useShallowEqualSelector tests', () => { | ||
test('should return the selected state', () => { | ||
const renderCount = testing.fn(); | ||
const shallowRenderCount = testing.fn(); | ||
const store = configureStore({ | ||
reducer: { | ||
counter: reducer, | ||
}, | ||
middleware: () => [], | ||
}); | ||
|
||
const {render} = rendererWith({store}); | ||
|
||
render( | ||
<> | ||
<TestComponent1 renderCallback={renderCount} /> | ||
<TestComponent2 renderCallback={shallowRenderCount} /> | ||
</>, | ||
); | ||
|
||
const counter = screen.getByTestId('counter'); | ||
const shallowCounter = screen.getByTestId('shallowCounter'); | ||
expect(counter).toHaveTextContent('0'); | ||
expect(shallowCounter).toHaveTextContent('0'); | ||
expect(renderCount).toHaveBeenCalledTimes(1); | ||
expect(shallowRenderCount).toHaveBeenCalledTimes(1); | ||
|
||
const updateCounter = screen.getByTestId('update'); | ||
fireEvent.click(updateCounter); | ||
|
||
expect(counter).toHaveTextContent('1'); | ||
expect(renderCount).toHaveBeenCalledTimes(2); | ||
expect(shallowCounter).toHaveTextContent('1'); | ||
expect(shallowRenderCount).toHaveBeenCalledTimes(2); | ||
|
||
fireEvent.click(updateCounter); | ||
|
||
expect(counter).toHaveTextContent('1'); | ||
expect(renderCount).toHaveBeenCalledTimes(3); | ||
expect(shallowCounter).toHaveTextContent('1'); | ||
expect(shallowRenderCount).toHaveBeenCalledTimes(2); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/* SPDX-FileCopyrightText: 2024 Greenbone AG | ||
* | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
import {useSelector, shallowEqual} from 'react-redux'; | ||
|
||
/** | ||
* A hook to use a redux selector with shallow equality check | ||
* | ||
* By default useSelector uses a strict equality check `===` to determine if the | ||
* state has changed. This hook uses a shallow equality check to determine if the | ||
* state has changed. | ||
* | ||
* @param {*} selector A redux selector | ||
* @returns {*} The selected state | ||
*/ | ||
const useShallowEqualSelector = selector => useSelector(selector, shallowEqual); | ||
|
||
export default useShallowEqualSelector; |