Javascript 如何正确地将useReducer操作传递给子级,而不会导致不必要的渲染

Javascript 如何正确地将useReducer操作传递给子级,而不会导致不必要的渲染,javascript,reactjs,react-hooks,Javascript,Reactjs,React Hooks,我不太清楚使用useReducerhook进行数据管理的最佳方式。我的主要目标是将样板文件减少到最低限度并保持代码可读性,同时在性能方面使用最佳方法并防止不必要的重新呈现 设置 我创建了我的应用程序的一个简化示例,基本上它是一个组件-一个可以选择它们的项目列表,以及一个可以切换项目组和重新加载数据集的组件 List.js 在一个简单的例子中,它看起来并不太糟糕,但是当您有几十个可能的操作时,所有这些操作都包装在带有deps数组的useCallback中,这似乎是不对的 它还需要在useffect

我不太清楚使用
useReducer
hook进行数据管理的最佳方式。我的主要目标是将样板文件减少到最低限度并保持代码可读性,同时在性能方面使用最佳方法并防止不必要的重新呈现

设置 我创建了我的应用程序的一个简化示例,基本上它是一个
组件-一个可以选择它们的项目列表,以及一个可以切换项目组和重新加载数据集的
组件

List.js 在一个简单的例子中,它看起来并不太糟糕,但是当您有几十个可能的操作时,所有这些操作都包装在带有deps数组的
useCallback
中,这似乎是不对的

它还需要在
useffect
中添加更多的dep(这是另一个问题)

下面是一个带有
useCallback
的示例

问题2 我无法在
之外完全提取reducer操作函数

但它是最优的,还是我应该使用一些完全不同的方法


你们是怎么用的?React文档和指南很好,有计数器增量和待办事项列表,但您如何在实际应用程序中实际使用它?

React redux还通过调用
dispatch来包装所有操作;这在使用
connect
HOC时被抽象掉,但在使用
useDispatch
hook时仍然是必需的。异步操作通常有一个函数签名
(…args)=>dispatch=>{}
,其中操作创建者返回一个接受redux提供的调度函数的函数,但redux需要中间件来处理这些函数。由于您实际上并没有使用Redux,所以您需要自己处理这个问题,可能需要使用这两种模式的组合来实现类似的用法

我建议作出以下修改:

  • 分离和隔离动作创建者,它们应该是返回动作对象的函数(或异步动作函数)
  • 创建处理异步操作的自定义分派函数
  • 组件渲染时正确记录(即在
    useffect
    hook的提交阶段和not组件主体的任何渲染阶段。请参阅)
  • 将自定义分派功能传递给子级,在子级中导入操作…在子级中分派操作
  • 仅有条件地呈现
    加载程序
    组件。当呈现
    加载程序
    列表
    中的一个或另一个时,另一个将卸载
  • Actions(Actions.js)

    useAncyReducer.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];
    };
    
    为什么
    usemo
    不需要依赖于
    useReducer
    dispatch函数

    注意

    React保证
    分派
    功能标识是稳定的,不会 重新渲染时的更改。这就是为什么可以安全地从
    useffect
    useCallback
    依赖项列表

    我们还希望提供一个稳定的
    调度
    功能参考

    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>
      );
    }
    
    import React,{useffect}来自“React”;
    从“/UseAncyReducer”导入useReducer;
    从“/Controls”导入控件;
    从“/List”导入列表;
    从“/Loader”导入加载程序;
    从“/constants”导入{ItemGroups};
    进口{
    开始,
    取得成功,
    集欧集团,
    选择_项,
    取消选择项目
    }从“/常量”;
    从“/actions”导入{fetchItems};
    导出默认函数App(){
    const[state,dispatch]=useReducer(itemsReducer{
    项目:[],
    选定:[],
    组:ItemGroups.PEOPLE,
    孤岛加载:false
    });
    const{items,group,selected,isLoading}=state;
    useffect(()=>{
    console.log(“对集团变更的使用影响”);
    调度(取件(组));
    }[组];
    React.useffect(()=>{
    
    console.log(“

    useCallback
    中包装您的“动作创建者”,以提供稳定的动作回调引用,(1)因为这是它的设计目的,它不是“黑客”,以及(2)IMO是所示选项中最干净的代码。每个渲染周期都会重新声明动作创建者。是否有一些子组件重新渲染的问题?您应该反转您的动作,以便
    dispatch
    包装返回的动作对象,然后您可以从外部定义动作创建者,它们不会成为渲染依赖项。您可以simp把
    dispatch
    传递给孩子们。是的,我也更喜欢第二种方法,我知道使用useCallback不是一种黑客行为,但它添加了太多不必要的代码,这是我想要避免的。至于反转操作-这是一个好主意,但并不适用于所有情况,因为有些操作函数会触发多个ac重新渲染-是的,我认为这是一个问题。我知道React实际上并没有修改所有渲染的DOM,但在应用程序的每个部分添加这种低效最终会增加。对我来说,这意味着知道它不会做任何愚蠢的事。:)
    import React, { useEffect, useReducer } from "react";
    
    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 fetchItemsFromAPI from "./api";
    
    import "./styles.css";
    
    const itemsReducer = (state, action) => {
      const { type, payload } = action;
    
      console.log(`reducer action "${type}" dispatched`);
    
      switch (type) {
        case FETCH_START:
          return {
            ...state,
            isLoading: true
          };
    
        case FETCH_SUCCESS:
          return {
            ...state,
            items: payload.items,
            isLoading: false
          };
    
        case SET_GROUP:
          return {
            ...state,
            selected: state.selected.length ? [] : state.selected,
            group: payload.group
          };
    
        case SELECT_ITEM:
          return {
            ...state,
            selected: [...state.selected, payload.id]
          };
    
        case DESELECT_ITEM:
          return {
            ...state,
            selected: state.selected.filter((id) => id !== payload.id)
          };
    
        default:
          throw new Error("Unknown action type in items reducer");
      }
    };
    
    export default function App() {
      const [state, dispatch] = useReducer(itemsReducer, {
        items: [],
        selected: [],
        group: ItemGroups.PEOPLE,
        isLoading: false
      });
    
      const { items, group, selected, isLoading } = state;
    
      const fetchItems = (group) => {
        dispatch({ type: FETCH_START });
    
        fetchItemsFromAPI(group).then((items) =>
          dispatch({
            type: FETCH_SUCCESS,
            payload: { items }
          })
        );
      };
    
      const setGroup = (group) => {
        dispatch({
          type: SET_GROUP,
          payload: { group }
        });
      };
    
      const selectItem = (id) => {
        dispatch({
          type: SELECT_ITEM,
          payload: { id }
        });
      };
    
      const deselectItem = (id) => {
        dispatch({
          type: DESELECT_ITEM,
          payload: { id }
        });
      };
    
      useEffect(() => {
        console.log("use effect on group change");
    
        fetchItems(group);
      }, [group]);
    
      console.log("<App /> render");
    
      return (
        <div className="App">
          <Controls {...{ group, fetchItems, setGroup }} />
          {isLoading ? (
            <Loader />
          ) : (
            <List {...{ items, selected, selectItem, deselectItem }} />
          )}
        </div>
      );
    }
    
    const setGroup = useCallback(
      (group) => {
        dispatch({
          type: SET_GROUP,
          payload: { group }
        });
      },
      [dispatch]
    );
    
    // ...
    export const fetchItems = (dispatch, group) => {
      dispatch({ type: FETCH_START });
    
      fetchItemsFromAPI(group).then((items) =>
        dispatch({
          type: FETCH_SUCCESS,
          payload: { items }
        })
      );
    };
    // ...
    
    import { fetchItems } from './actions';
    
    const Child = ({ dispatch, group }) => {
      fetchItems(dispatch, group);
      // ...
    };
    
    // ...
    const App = () => {
      const [{ items, group, selected, isLoading }, dispatch] = useReducer(
        itemsReducer,
        itemReducerDefaults
      );
    
      useEffect(() => {
        fetchItems(dispatch, group);
      }, [group, dispatch]);
    
      return (
        <div className="App">
          <Controls {...{ group, dispatch }} />
          {isLoading ? <Loader /> : <List {...{ items, selected, dispatch }} />}
        </div>
      );
    };
    
    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 }
        })
      );
    };
    
    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];
    };
    
    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>
      );
    }
    
    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>
      );
    };
    
    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>
      );
    };
    
    const Loader = () => {
      React.useEffect(() => {
        console.log("<Loader /> render");
      });
    
      return <div>Loading data...</div>;
    };