From a6d9b92655410fa47ed4b5e5ad06ae3dd2fe4e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Thu, 6 Jun 2024 14:15:49 +0200 Subject: [PATCH] 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 --- .../__tests__/useShallowEqualSelector.jsx | 96 +++++++++++++++++++ src/web/hooks/useShallowEqualSelector.js | 20 ++++ 2 files changed, 116 insertions(+) create mode 100644 src/web/hooks/__tests__/useShallowEqualSelector.jsx create mode 100644 src/web/hooks/useShallowEqualSelector.js diff --git a/src/web/hooks/__tests__/useShallowEqualSelector.jsx b/src/web/hooks/__tests__/useShallowEqualSelector.jsx new file mode 100644 index 0000000000..8f0f9fa382 --- /dev/null +++ b/src/web/hooks/__tests__/useShallowEqualSelector.jsx @@ -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 ( +
+
{state.value}
+ +
+ ); +}; + +const TestComponent2 = ({renderCallback}) => { + const state = useShallowEqualSelector(state => state.counter); + renderCallback(); + return ( +
+
{state.value}
+
+ ); +}; + +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( + <> + + + , + ); + + 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); + }); +}); diff --git a/src/web/hooks/useShallowEqualSelector.js b/src/web/hooks/useShallowEqualSelector.js new file mode 100644 index 0000000000..5cf2ca5dbc --- /dev/null +++ b/src/web/hooks/useShallowEqualSelector.js @@ -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;