Search code examples
javascriptreactjsreduxreact-hooksredux-toolkit

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

  • 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