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
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