Skip to content Skip to sidebar Skip to footer

How To Properly Pass Usereducer Actions Down To Children Without Causing Unnecessary Renders

I can't quite figure out the optimal way to use useReducer hook for data management. My primary goal is to reduce (heh) the boilerplate to minimum and maintain code readability, wh

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:

  1. De-couple and isolate your action creators, they should be functions that return action objects (or asynchronous action functions).
  2. Create a custom dispatch function that handles asynchronous actions.
  3. 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.
  4. Pass the custom dispatch function to children, import actions in children... dispatch actions in children. How to avoid passing callbacks down.
  5. Only conditionally render the Loader component. When you render one or the other of Loader and List 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?

useReducer

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 the useEffect or useCallback 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>;
};

Edit how-to-properly-pass-usereducer-actions-down-to-children-without-causing-unneces

Post a Comment for "How To Properly Pass Usereducer Actions Down To Children Without Causing Unnecessary Renders"