道招

转译:使用react hooks优化回调函数在组件间的传递,useState,useReducer?

如果您发现本文排版有问题,可以先点击下面的链接切换至老版进行查看!!!

转译:使用react hooks优化回调函数在组件间的传递,useState,useReducer?

我们先看一下使用useStatehooks写的todoList组件,里面我们需要层层传递回调函数。

import React, { useState } from "react";

const AddTodoBtn = ({ onAddTodo }) => (
  <div className="action-add">
    <button onClick={onAddTodo}>Add new todo</button>
  </div>
);

const RemoveAllBtn = ({ onRemoveAll }) => (
  <div className="action-remove-all">
    <button onClick={onRemoveAll}>Remove all todos</button>
  </div>
);

// This component and every one placed between the TodoList
// and the final component which will use a TodoList callback
// should pass it down.
const Actions = ({ onAddTodo, onRemoveAll }) => (
  <div className="actions-container">
    <AddTodoBtn onAddTodo={onAddTodo} />
    <RemoveAllBtn onRemoveAll={onRemoveAll} />
    //...more actions
  </div>
);

const MenuOnTop = ({ onAddTodo, onRemoveAll }) => (
  <div className="menu-on-top">
    <Actions onAddTodo={onAddTodo} onRemoveAll={onRemoveAll} />
    <Img url="some-path" />
  </div>
);

const TodoList = () => {
  const [todos, setTodos] = useState([]);
  const addTodo = () => setTodos([...todos, {}]);
  const removeAll = () => setTodos([]);

  return (
    <div className="todo-list">
      <MenuOnTop onAddTodo={addTodo} onRemoveAll={removeAll}/>
      //...todos
    </div>
  );
};

file 我们可以看到在父组件TodoList里面定义的两个方法addTodoremoveAll,它们被一层一层传递到子组件AddTodoBtnRemoveAllBtn,也就是真正执行该方法的组件。

TodoList -> MenuOnTop -> Actions -> AddTodoBtn/RemoveAllBtn

为了避免遍历层次结构中的每个组件,我们可以通过带有useContext Hook的React Context传递“ API对象”。

import React, { useState, createContext, useContext } from "react";

const AddTodoBtn = () => {
  const api = useContext(TodosApi);

  return (
    <div className="action-add">
      <button onClick={api.addTodo}>Add new todo</button>
    </div>
  );
};

const RemoveAllBtn = () => {
  const api = useContext(TodosApi);

  return (
    <div className="action-remove-all">
      <button onClick={api.removeAll}>Remove all todos</button>
    </div>
  );
};

const Actions = () => (
  <div className="actions-container">
    <AddTodoBtn />
    <RemoveAllBtn />
    //...more actions
  </div>
);

const MenuOnTop = () => (
  <div className="menu-on-top">
    <Actions />
    <Img url="some-path" />
  </div>
);

const TodosApi = createContext(null);

const TodoList = () => {
  const [todos, setTodos] = useState([]);
  const addTodo = () => setTodos([...todos, {}]);
  const removeAll = () => setTodos([]);

  const api = { addTodo, removeAll };

  return (
    <div className="todo-list">
      <TodosApi.Provider value={api}>
        <MenuOnTop />
        //...state.todos
      </TodosApi.Provider>
    </div>
  );
};

这样我们可以在子组件AddTodoBtnRemoveAllBtn里面直接获取到最外层父组件TodoList里面定义的addTodoremoveAll方法。

但是仍然有些问题,问题在于API对象在每次重新渲染时都会更改,因此从上下文读取它的所有组件也会被重新渲染。

官方团队推荐我们使用useReducer来替代useState, 使用useReducer管理state可以使我们仅向下传递,并且由于传递过程在渲染之间不会更改,因此不会重新渲染从上下文中读取状态的组件。

我们的代码就应该这样写了。

import React, { useReducer, createContext, useContext } from "react";

const AddTodoBtn = () => {
  const dispatch = useContext(TodosDispatch);

  return (
    <div className="action-add">
      <button onClick={() => dispatch({ type: "add" })}>Add new todo</button>
    </div>
  );
};

const RemoveAllBtn = () => {
  const dispatch = useContext(TodosDispatch);

  return (
    <div className="action-remove-all">
      <button onClick={() => dispatch({ type: "removeAll" })}>Remove all todos</button>
    </div>
  );
};

const Actions = () => (
  <div className="actions-container">
    <AddTodoBtn />
    <RemoveAllBtn />
    //...more actions
  </div>
);

const MenuOnTop = () => (
  <div className="menu-on-top">
    <Actions />
    <Img url="some-path" />
  </div>
);

const TodosDispatch = createContext(null);

const TodoList = () => {

  const reducer = (state, action) => {
    switch (action.type) {
      case "add":
        return { todos: [...state.todos, {}] };
      case "removeAll":
        return { todos: [] };
      default:
        return state;
    }
  }; 

  const [state, dispatch] = useReducer(reducer);

  return (
    <div className="todo-list">
      <TodosDispatch.Provider value={dispatch}>
        <MenuOnTop />
        //...state.todos
      </TodosDispatch.Provider>
    </div>
  );
};

通过这个改动,我们发现有点小问题:

  1. 我们不得不把无状态组件AddTodoBtn和I RemoveAllBtn变成了“有状态组件”了,它们其实只是想要点击的回调而已。
  2. 我们不得不用useReducerhook了,在上面的简单示例里面确实没有问题,但是如果复杂场景的话,之前依据useState做的工作可能无法再复用了。
我们怎么解决这些问题呢?

结合上面的两个示例我们准备这样做。

  1. 将需要的变量通过上下文传递给组件
  2. 用一个包含我们需要的回调函数的“API对象”,这些回调函数可以用useState或这useReducer都行
  3. 缓存这个“API对象”,确保在组件重新渲染也不会改变
  4. 确保“API对象”里面的回调不依赖当前作用域
怎么缓存

缓存“API对象”是我们需要除了用useMemo缓存这个对象外,还要useCallback缓存它里面包含的回调

import React, { useState, createContext, useMemo, useCallback } from "react";

const TodosApi = createContext(null);

const TodoList = () => {
  const [todos, setTodos] = useState([]);
  // 🔴 THIS IS WRONG: The callback memoization depends on the scope
  const addTodo = useCallback(() => setTodos([...todos, {}]));
  const removeAll = useCallback(() => setTodos([]));

  const getApi = useMemo(() => ({ addTodo, removeAll }));

  return (
    <div className="todo-list">
      <TodosApi.Provider value={getApi()}>
        <MenuOnTop />
        //...state.todos
      </TodosApi.Provider>
    </div>
  );
};

好的,我们已经缓存了API对象,因此它在重渲染时也不会改变,但是取决于作用域的操作(比如addTodo取决于待办事项状态值)在重渲染上将会不一致,因为状态变量的值不会改变。

我们可以将todos作为useMemouseCallback的依赖传给第二个参数,这样todos变化时“API对象”和回调就会随之改变了。

import React, { useState, createContext, useMemo, useCallback } from "react";

const TodosApi = createContext(null);

const TodoList = () => {
  const [todos, setTodos] = useState([]);
  // 🔶 It works but still it's not the solution we wanted 
  const addTodo = useCallback(() => setTodos([...todos, {}]), [todos]);
  const removeAll = useCallback(() => setTodos([]), []);

  // We have to add all scope variables used by the callbacks
  // to the array of dependencies
  const getApi = useMemo(() => ({ addTodo, removeAll }), [todos]);

  return (
    <div className="todo-list">
      <TodosApi.Provider value={getApi()}>
        <MenuOnTop />
        //...state.todos
      </TodosApi.Provider>
    </div>
  );
};

如果“API对象”只依赖它定义时的作用域的话,我们只是局部缓存。

If we want our API object to never change between rerenders, we need our callbacks to be independent of the scope. We can do this by updating the state with functional updates: We can pass a function to setState, the function will receive the previous value of the state, and return the updated value.

如果我们希望“API对象”在重渲染的永远不改变的话,我们需要让回调独立于作用域。我们将更新的state改用函数更新的方式:我们传递一个函数给setState(而不再是直接传入新的state),这个函数会收到旧的state,它需要返回新的state。

import React, { useState, createContext, useMemo, useCallback } from "react";

const TodosApi = createContext(null);

const TodoList = () => {
  const [todos, setTodos] = useState([]);
  // Independent from its scope
  const addTodo = useCallback(() => setTodos(prevTodos => [...prevTodos, {}]));
  const removeAll = useCallback(() => setTodos([]), []);

  // Now there isn't any dependency with the scope, 
  // the API object won't change!
  const getApi = useMemo(() => ({ addTodo, removeAll }), []);

  return (
    <div className="todo-list">
      <TodosApi.Provider value={getApi()}>
        <MenuOnTop />
        //...state.todos
      </TodosApi.Provider>
    </div>
  );
};

现在我们的“API对象”彻底独立于作用域了,它就是做dispatch的,重渲染时它再也不会改变了。

总结下:

To avoid passing callbacks down through the component hierarchy, we can follow the suggested pattern of passing the dispatch function of the useReducer using the Context, but we end up being forced to manage the state via a reducer, and we have to spread some knowledge about the state through the components that use this callbacks. We suggest to do this by passing a memoized API object using the context. To achieve the complete invariability of the API object, we use functional updates so we don’t depend on the scope of the callbacks.

为了避免回调函数在组件中层层传递,我们先尝试了官方推荐的解决方案——useReducer,但是因为鉴于要强制使用reducer的方式管理state,我们放弃了这种方案,我们不得不继续使用回调来传递state。 后来我们建议通过context传递一个缓存的“API对象”,为了确保“API对象”的不变性,我们改用函数更新来解除了对回调作用域的依赖。

本文绝大部分内容转自Passing callbacks down with React Hooks

更新时间:
上一篇:参考教程实现WordPress更新博文通知钩子插件下一篇:改造富文本编辑器wangEditor成react组件

相关文章

WordPress博客项目改用react前端展示

之前自己的主打技术栈是vue全家桶,所以将自己的wordpress博客改成了vue版本服务端渲染,现在因为公司需要将我的项目从vue转到react,本人后面可能也就要主打eact技术栈了。 我记 阅读更多…

改造富文本编辑器wangEditor成react组件

我们知道wangEditor常用的功能是editor实例的 txt.html() 和 txt.text() 方法,尤其是 txt.html() 方法,这是一个类似与jQuery常用的那种get和se 阅读更多…

Did you mean to use React.forwardRef()?搞懂react的createRef和forwardRef

最近在使用react过程中发现在使用ref时的一些场景,自己初步感觉react的ref没有vue那么强大。 现在我就简单看下怎么使用ref? createRef 我们直接看源码 / 阅读更多…

webpack笔记——在html-webpack-plugin插件中提供给其它插件是使用的hooks

最近在这段时间刚好在温故下webpack源码,webpack5都出来了,4还不再学习下? 这次顺便学习下webpack的常用插件html-webpack-plugin。 发现这个插件里面还额外加入了 阅读更多…

关注道招网公众帐号
道招开发者二群