'Using dispatch on input change event causes rerendering the whole page - Redux Toolkit

I am new to React and Redux and I am having this problem:

I am using Redux Toolkit. I have styled radio inputs and when one is checked it must change a global state with its value but must not be rerendered when the global state changes because on rerender, it spoils the style (CSS :checked selector works inproperly). Only the elements which use that state must change, ie. the Button. How can I prevent them from rerendering when the global state changes? What am I doing wrong? Thanks in advance.

const dispatch = useDispatch()
const { regType } = useSelector((state) => state.regTypes)

const handleChange = (e) => {
  dispatch(changeRegType(e.target.value))
}

return (
  <form>
    <StyledRadio name="registrationType" id="private" value="private" onChange={handleChange} >
    <StyledRadio name="registrationType" id="company" value="company" onChange={handleChange} />
    <Button type="submit" disabled={!!regType ? true : false}>Next</Button>
  </form>
)

Slice

export const registrationTypesSlice = createSlice({
  name: "registrationType",
  initialState: {
    regType: "",
  },
  reducers: {
    changeRegType: (state, action) => {
      state.regType = action.payload
    },
  },
})

export const { changeRegType } = registrationTypesSlice.actions
export default registrationTypesSlice.reducer


Solution 1:[1]

Short answer: you need to use React.memo HOC wrap your StyledRadio component. And, use useCallback hook create a memoized version of the handleChange event handler.

Long answer, see below code:

index.tsx:

import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { StyledRadio } from './StyledRadio';

const changeRegType = (v) => ({ type: 'CHANGE_REG_TYPE', payload: v });

export function Comp() {
  const dispatch = useDispatch();
  const { regType } = useSelector((state: any) => state.regTypes);
  console.log('regType: ', regType);

  const handleChange = useCallback(
    (e) => {
      const { value } = e.target;
      dispatch(changeRegType(value));
    },
    [dispatch],
  );

  return (
    <form>
      <StyledRadio name="registrationType" data-testid="private" id="private" value="private" onChange={handleChange} />
      <StyledRadio name="registrationType" data-testid="company" id="company" value="company" onChange={handleChange} />
      <button type="submit" disabled={!!regType ? true : false}>
        Next
      </button>
    </form>
  );
}

StyledRadio.tsx:

import React from 'react';

export const StyledRadio = React.memo((props: React.ComponentPropsWithoutRef<'input'>) => {
  console.count('StyledRadio render');
  return <input {...props} type="radio" />;
});

index.test.tsx:

import { configureStore } from '@reduxjs/toolkit';
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { Comp } from './';

const store = configureStore({
  reducer: (state = { regTypes: { regType: '' } }, action) => {
    switch (action.type) {
      case 'CHANGE_REG_TYPE':
        return { ...state, regTypes: { regType: action.payload } };
      default:
        return state;
    }
  },
});

describe('71504950', () => {
  test('should pass', () => {
    render(
      <Provider store={store}>
        <Comp />
      </Provider>,
    );
    fireEvent.click(screen.getByTestId('private'), { target: { value: 'private' } });
  });
});

Test result:

 PASS   redux-toolkit-example  packages/redux-toolkit-example/stackoverflow/71504950/index.test.tsx
  71504950
    ? should pass (56 ms)

  console.log
    regType:

      at Comp (packages/redux-toolkit-example/stackoverflow/71504950/index.tsx:10:11)

  console.count
    StyledRadio render: 1

      at packages/redux-toolkit-example/stackoverflow/71504950/StyledRadio.tsx:4:11

  console.count
    StyledRadio render: 2

      at packages/redux-toolkit-example/stackoverflow/71504950/StyledRadio.tsx:4:11

  console.log
    regType:  private

      at Comp (packages/redux-toolkit-example/stackoverflow/71504950/index.tsx:10:11)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.303 s

As you can see, when the Comp component first render the logs:

// first render
regType:
StyledRadio render: 1
StyledRadio render: 2
// second render
regType:  private

After triggering the radio change event, change the state via the CHANGE_REG_TYPE action. The Comp will re-render, at this time, the props of the StyledRadio are the same as the previous render, so the React.memo will skip re-render the component.

If you remove useCallback or React.memo, you will get the logs:

// first render
regType:
StyledRadio render: 1
StyledRadio render: 2
// second render
regType:  private
StyledRadio render: 3
StyledRadio render: 4

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 slideshowp2