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

export const setGroup = (group) => ({
  type: SET_GROUP,
  payload: { group }
});

export const selectItem = (id) => ({
  type: SELECT_ITEM,
  payload: { id }
});

export const deselectItem = (id) => ({
  type: DESELECT_ITEM,
  payload: { id }
});

export const fetchItems = (group) => (dispatch) => {
  dispatch({ type: FETCH_START });

  fetchItemsFromAPI(group).then((items) =>
    dispatch({
      type: FETCH_SUCCESS,
      payload: { items }
    })
  );
};

useAsyncReducer.js

const asyncDispatch = (dispatch) => (action) =>
  action instanceof Function ? action(dispatch) : dispatch(action);

export default (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

import React, { useEffect } from "react";
import useReducer from "./useAsyncReducer";

import Controls from "./Controls";
import List from "./List";
import Loader from "./Loader";

import { ItemGroups } from "./constants";

import {
  FETCH_START,
  FETCH_SUCCESS,
  SET_GROUP,
  SELECT_ITEM,
  DESELECT_ITEM
} from "./constants";
import { fetchItems } from "./actions";

export default function App() {
  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 (
    <div className="App">
      <Controls {...{ group, dispatch }} />
      {isLoading && <Loader />}
      <List {...{ items, selected, dispatch }} />
    </div>
  );
}

Controls.js

import React, { memo } from "react";
import { ItemGroups } from "./constants";
import { setGroup, fetchItems } from "./actions";

const Controls = ({ dispatch, group }) => {
  React.useEffect(() => {
    console.log("<Controls /> render");
  });

  return (
    <div className="Controls">
      <label>
        Select group
        <select
          value={group}
          onChange={(e) => dispatch(setGroup(e.target.value))}
        >
          <option value={ItemGroups.PEOPLE}>{ItemGroups.PEOPLE}</option>
          <option value={ItemGroups.TREES}>{ItemGroups.TREES}</option>
        </select>
      </label>
      <button onClick={() => dispatch(fetchItems(group))}>Reload data</button>
    </div>
  );
};

List.js

import React, { memo } from "react";
import { deselectItem, selectItem } from "./actions";

const List = ({ dispatch, items, selected }) => {
  React.useEffect(() => {
    console.log("<List /> render");
  });

  return (
    <ul className="List">
      {items.map(({ id, name }) => (
        <li key={`item-${name.toLowerCase()}`}>
          <label>
            <input
              type="checkbox"
              checked={selected.includes(id)}
              onChange={(e) =>
                dispatch((e.target.checked ? selectItem : deselectItem)(id))
              }
            />
            {name}
          </label>
        </li>
      ))}
    </ul>
  );
};

Loader.js

const Loader = () => {
  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"