内容简介:我们先看一下使用为了避免遍历层次结构中的每个组件,我们可以通过带有useContext Hook的React Context传递“ API对象”。
我们先看一下使用 useState
hooks写的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> ); };
我们可以看到在父组件 TodoList 里面定义的两个方法 addTodo
和 removeAll
,它们被一层一层传递到子组件 AddTodoBtn 和 RemoveAllBtn ,也就是真正执行该方法的组件。
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> ); };
这样我们可以在子组件 AddTodoBtn 和 RemoveAllBtn 里面直接获取到最外层父组件 TodoList 里面定义的 addTodo
和 removeAll
方法。
但是仍然有些问题,问题在于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> ); };
通过这个改动,我们发现有点小问题:
- 我们不得不把无状态组件 AddTodoBtn 和I RemoveAllBtn 变成了“有状态组件”了,它们其实只是想要点击的回调而已。
- 我们不得不用
useReducer
hook了,在上面的简单示例里面确实没有问题,但是如果复杂场景的话,之前依据useState
做的工作可能无法再复用了。
我们怎么解决这些问题呢?
结合上面的两个示例我们准备这样做。
- 将需要的变量通过上下文传递给组件
- 用一个包含我们需要的回调函数的“API对象”,这些回调函数可以用
useState
或这useReducer
都行 - 缓存这个“API对象”,确保在组件重新渲染也不会改变
- 确保“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
作为 useMemo
和 useCallback
的依赖传给第二个参数,这样 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
以上所述就是小编给大家介绍的《转译:使用react hooks优化回调函数在组件间的传递,useState,useReducer?》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- CoffeeScript 2.3.0 发布,JavaScript 转译语言
- JavaScript 基础:Babel 转译 class 过程窥探
- CoffeeScript 2.5.0 发布,JavaScript 转译语言
- [OC] 关于block回调、高阶函数“回调再调用”及项目实践
- 即使回调IsOneWay,WCF客户端也会因回调而死锁
- Java 回调机制解读
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。