Why creating a custom hook always triggers re-renderings and using useDispatch and useSelector directly do not?
There is a pattern I was using on a project where most of the state resides on redux. I also try to keep the application logic close to redux, so I use selectors and I wrap all the possible actions to be taken into custom hooks.
Running some profiling tests I noticed that, when I use my custom hooks (example below) all the components that use them re-render when the redux state changes:
export const useDefinitionForm = () => {
const { description, step } = useAppSelector((state) => {
const { name, color, icon, duration } = state.descriptionForm;
return { description: { name, color, icon, duration }, step: state.descriptionForm.step };
});
const dispatch = useAppDispatch();
return { description, step, actions: bindActionCreators(actions, dispatch) };
};
However, when I use the useDispatch and useSelector hooks directly on the target component, then those components only re-render when the specific redux section changes:
const DurationSection = () => {
const dispatch = useAppDispatch();
const duration = useAppSelector((state) => state.descriptionForm.duration);
return (
<Section title="Expected duration" hint={duration} step="duration">
<Typography variant="subtitle1">How much will this task usually last?</Typography>
<DurationSlider
value={duration}
onChange={(value) => dispatch(setDuration(value))}
valueLabelDisplay="on"
/>
</Section>
);
};
Why is this? How can I change the custom hooks so they are as performant as using the hooks directly on the component? I try to keep the components as dumb as possible.
The selector in your custom hook returns a new object every time:
export const useDefinitionForm = () => {
const { description, step } = useAppSelector((state) => {
const { name, color, icon, duration } = state.descriptionForm;
return { description: { name, color, icon, duration }, step: state.descriptionForm.step };
});
const dispatch = useAppDispatch();
//you are returning a new object here every time
return { description, step, actions: bindActionCreators(actions, dispatch) };
};
The selector in your other example does not:
const duration = useAppSelector(
(state) =>
//you are only returning a value from state here
state.descriptionForm.duration
);
All functions you pass to useSelecor will be executed every time the state changed and if that function returns a changed value your component will re render.
You can use reselect to memoize the result of previous selectors and only return a new object if any of the re used selector(s) return a changed value:
const selectDescriptionForm = state => state.descriptionForm;
//you can create selectors to selectStep, selectDuration, selectIcon ...
// and use those instead if this still causes needless re renders
const selectDuration = createSelector(
[selectDescriptionForm],
//the following function will only be executed if
// if selectDescriptionForm returns a changed value
({ name, color, icon, duration, step })=>({
description: {
name, color, icon, duration
}, step
})
)
export const useDefinitionForm = () => {
const { description, step } = useAppSelector(selectDuration);
const dispatch = useAppDispatch();
//making sure actions is not needlessly re created
const actions = useMemo(
()=>bindActionCreators(actions, dispatch),
[dispatch]
)
return { description, step, actions };
};
Here is an updated version selecting each item of description form you need and if any of the items change will re create the component data:
const selectDescriptionForm = (state) =>
state.descriptionForm;
const selectName = createSelector(
[selectDescriptionForm],
(descriptionForm) => descriptionForm.name
);
const selectColor = createSelector(
[selectDescriptionForm],
(descriptionForm) => descriptionForm.color
);
const selectIcon = createSelector(
[selectDescriptionForm],
(descriptionForm) => descriptionForm.icon
);
const selectDuration = createSelector(
[selectDescriptionForm],
(descriptionForm) => descriptionForm.duration
);
const selectStep = createSelector(
[selectDescriptionForm],
(descriptionForm) => descriptionForm.step
);
const selectCompoentData = createSelector(
[
selectName,
selectColor,
selectIcon,
selectDuration,
selectStep,
],
//the following function will only be executed if
// any of the functions in the previous list returns
// a changed value
(name, color, icon, duration, step) => ({
description: {
name,
color,
icon,
duration,
},
step,
})
);
export const useDefinitionForm = () => {
const { description, step } = useAppSelector(
selectCompoentData
);
const dispatch = useAppDispatch();
//making sure actions is not needlessly re created
const actions = useMemo(
() => bindActionCreators(actions, dispatch),
[dispatch]
);
return { description, step, actions };
};