How To Properly Pass Usereducer Actions Down To Children Without Causing Unnecessary Renders
Solution 1:
React-redux works by also wrapping all the actions with a call to dispatch
; this is abstracted away when using the connect
HOC, but still required when using the useDispatch
hook. Async actions typically have a function signature (...args) => dispatch => {}
where the action creator instead returns a function that accepts the dispatch function provided by redux, but redux requires middleware to handle these. Since you are not actually using Redux you'd need to handle this yourself, likely using a combination of both patterns to achieve similar usage.
I suggest the following changes:
- De-couple and isolate your action creators, they should be functions that return action objects (or asynchronous action functions).
- Create a custom dispatch function that handles asynchronous actions.
- Correctly log when a component renders (i.e. during the commit phase in an
useEffect
hook and not during any render phase in the component body. See this lifecycle diagram. - Pass the custom dispatch function to children, import actions in children... dispatch actions in children. How to avoid passing callbacks down.
- Only conditionally render the
Loader
component. When you render one or the other ofLoader
andList
the other is unmounted.
Actions (actions.js)
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from"./constants";
import fetchItemsFromAPI from"./api";
exportconstsetGroup = (group) => ({
type: SET_GROUP,
payload: { group }
});
exportconstselectItem = (id) => ({
type: SELECT_ITEM,
payload: { id }
});
exportconstdeselectItem = (id) => ({
type: DESELECT_ITEM,
payload: { id }
});
exportconstfetchItems = (group) => (dispatch) => {
dispatch({ type: FETCH_START });
fetchItemsFromAPI(group).then((items) =>dispatch({
type: FETCH_SUCCESS,
payload: { items }
})
);
};
useAsyncReducer.js
constasyncDispatch = (dispatch) => (action) =>
action instanceofFunction ? action(dispatch) : dispatch(action);
exportdefault (reducer, initialArg, init) => {
const [state, syncDispatch] = React.useReducer(reducer, initialArg, init);
const dispatch = React.useMemo(() =>asyncDispatch(syncDispatch), []);
return [state, dispatch];
};
Why doesn't useMemo
need a dependency on useReducer
dispatch function?
Note
React guarantees that
dispatch
function identity is stable and won’t change on re-renders. This is why it’s safe to omit from theuseEffect
oruseCallback
dependency list.
We want to also provide a stable dispatch
function reference.
App.js
importReact, { useEffect } from"react";
import useReducer from"./useAsyncReducer";
importControlsfrom"./Controls";
importListfrom"./List";
importLoaderfrom"./Loader";
import { ItemGroups } from"./constants";
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from"./constants";
import { fetchItems } from"./actions";
exportdefaultfunctionApp() {
const [state, dispatch] = useReducer(itemsReducer, {
items: [],
selected: [],
group: ItemGroups.PEOPLE,
isLoading: false
});
const { items, group, selected, isLoading } = state;
useEffect(() => {
console.log("use effect on group change");
dispatch(fetchItems(group));
}, [group]);
React.useEffect(() => {
console.log("<App /> render");
});
return (
<divclassName="App"><Controls {...{ group, dispatch }} />
{isLoading && <Loader />}
<List {...{ items, selected, dispatch }} /></div>
);
}
Controls.js
importReact, { memo } from"react";
import { ItemGroups } from"./constants";
import { setGroup, fetchItems } from"./actions";
constControls = ({ dispatch, group }) => {
React.useEffect(() => {
console.log("<Controls /> render");
});
return (
<divclassName="Controls"><label>
Select group
<selectvalue={group}onChange={(e) => dispatch(setGroup(e.target.value))}
>
<optionvalue={ItemGroups.PEOPLE}>{ItemGroups.PEOPLE}</option><optionvalue={ItemGroups.TREES}>{ItemGroups.TREES}</option></select></label><buttononClick={() => dispatch(fetchItems(group))}>Reload data</button></div>
);
};
List.js
importReact, { memo } from"react";
import { deselectItem, selectItem } from"./actions";
constList = ({ dispatch, items, selected }) => {
React.useEffect(() => {
console.log("<List /> render");
});
return (
<ulclassName="List">
{items.map(({ id, name }) => (
<likey={`item-${name.toLowerCase()}`}><label><inputtype="checkbox"checked={selected.includes(id)}onChange={(e) =>
dispatch((e.target.checked ? selectItem : deselectItem)(id))
}
/>
{name}
</label></li>
))}
</ul>
);
};
Loader.js
constLoader = () => {
React.useEffect(() => {
console.log("<Loader /> render");
});
return<div>Loading data...</div>;
};
Post a Comment for "How To Properly Pass Usereducer Actions Down To Children Without Causing Unnecessary Renders"