如何操作上下文 - 将函数附加到上下文或在钩子中包装调度?

IT技术 reactjs react-hooks react-context
2021-05-02 11:47:39

我想知道操作和公开新的 React Context 的推荐最佳实践是什么。

操作上下文状态的最简单方法似乎是将一个函数附加到上下文中,该函数在调用后会调度 ( usereducer) 或 setstate ( useState) 以更改其内部值。

export const TodosProvider: React.FC<any> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, null, init);

  return (
    <Context.Provider
      value={{
        todos: state.todos,
        fetchTodos: async id => {
          const todos = await getTodos(id);
          console.log(id);
          dispatch({ type: "SET_TODOS", payload: todos });
        }
      }}
    >
      {children}
    </Context.Provider>
  );
};

export const Todos = id => {
  const { todos, fetchTodos } = useContext(Context);
  useEffect(() => {
    if (fetchTodos) fetchTodos(id);
  }, [fetchTodos]);
  return (
    <div>
      <pre>{JSON.stringify(todos)}</pre>
    </div>
  );
};

然而,有人告诉我直接暴露和使用 react 上下文对象可能不是一个好主意,并被告知将它包装在一个钩子中。

export const TodosProvider: React.FC<any> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, null, init);

  return (
    <Context.Provider
      value={{
        dispatch,
        state
      }}
    >
      {children}
    </Context.Provider>
  );
};

const useTodos = () => {
  const { state, dispatch } = useContext(Context);
  const [actionCreators, setActionCreators] = useState(null);

  useEffect(() => {
    setActionCreators({
      fetchTodos: async id => {
        const todos = await getTodos(id);
        console.log(id);
        dispatch({ type: "SET_TODOS", payload: todos });
      }
    });
  }, []);

  return {
    ...state,
    ...actionCreators
  };
};

export const Todos = ({ id }) => {
  const { todos, fetchTodos } = useTodos();
  useEffect(() => {
    if (fetchTodos && id) fetchTodos(id);
  }, [fetchTodos]);

  return (
    <div>
      <pre>{JSON.stringify(todos)}</pre>
    </div>
  );
};

我在这里为这两种变体制作了运行代码示例:https : //codesandbox.io/s/mzxrjz0v78?fontsize=14

所以现在我有点困惑,这两种方法中哪一种是正确的方法?

2个回答

useContext直接在组件中使用绝对没有问题然而,它强制必须使用上下文值的组件知道要使用什么上下文。

如果你在应用中有多个组件想要使用 TodoProvider 上下文,或者你的应用中有多个上下文,你可以使用自定义钩子稍微简化它

使用上下文时还必须考虑的另一件事是,您不应该在每次渲染时创建新对象,否则所有正在使用的组件context都会重新渲染,即使什么都不会改变。为此,您可以使用useMemohook

const Context = React.createContext<{ todos: any; fetchTodos: any }>(undefined);

export const TodosProvider: React.FC<any> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, null, init);
  const context = useMemo(() => {
    return {
      todos: state.todos,
      fetchTodos: async id => {
        const todos = await getTodos(id);
        console.log(id);
        dispatch({ type: "SET_TODOS", payload: todos });
      }
    };
  }, [state.todos, getTodos]);
  return <Context.Provider value={context}>{children}</Context.Provider>;
};

const getTodos = async id => {
  console.log(id);
  const response = await fetch(
    "https://jsonplaceholder.typicode.com/todos/" + id
  );
  return await response.json();
};
export const useTodos = () => {
  const todoContext = useContext(Context);
  return todoContext;
};
export const Todos = ({ id }) => {
  const { todos, fetchTodos } = useTodos();
  useEffect(() => {
    if (fetchTodos) fetchTodos(id);
  }, [id]);
  return (
    <div>
      <pre>{JSON.stringify(todos)}</pre>
    </div>
  );
};

工作演示

编辑:

既然getTodos只是一个不能改变的函数,那么在 中使用它作为更新参数有意义useMemo吗?

getTodos如果 getTodos 方法正在更改并在功能组件中被调用,则传递给 useMemo 中的依赖项数组是有意义的。通常,您会记住方法 usinguseCallback以便它不会在每次渲染时创建,而是仅当它的任何来自封闭范围的依赖项更改以更新其词法范围内的依赖项时。现在在这种情况下,您需要将其作为参数传递给依赖项数组。

但是,在您的情况下,您可以省略它。

还有你将如何处理初始效果。假设您要在提供程序挂载时在 useEffect 钩子中调用 `getTodos' 吗?你能记住那个电话吗?

您只需在初始安装时调用的 Provider 中产生效果

export const TodosProvider: React.FC<any> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, null, init);
  const context = useMemo(() => {
    return {
      todos: state.todos,
      fetchTodos: async id => {
        const todos = await getTodos(id);
        console.log(id);
        dispatch({ type: "SET_TODOS", payload: todos });
      }
    };
  }, [state.todos]);
  useEffect(() => {
      getTodos();
  }, [])
  return <Context.Provider value={context}>{children}</Context.Provider>;
};

我认为没有官方答案,所以让我们在这里尝试使用一些常识。我觉得useContext直接使用完全没问题,我不知道是谁告诉你不要的,也许他/她应该指出官方文档。如果不应该使用它,那么 React 团队为什么要创建该钩子?:)

然而,我可以理解,试图避免创建一个巨大的对象,如value中的Context.Provider,一个将状态与操作它的函数混合在一起的对象,可能具有像您的示例那样的异步效果。

但是,在您的重构中,您useState为动作创建者引入了一个非常奇怪且绝对不必要的内容,您只是在第一种方法中定义了内联。在我看来,你正在寻找useCallback那么,你为什么不这样混合呢?

  const useTodos = () => {
    const { state, dispatch } = useContext(Context);
    const fetchTodos = useCallback(async id => {
      const todos = await getTodos(id)
      dispatch({ type: 'SAVE_TODOS', payload: todos })
    }, [dispatch])

    return {
      ...state,
      fetchTodos
    };
}

您的调用代码不需要那种奇怪的检查来验证fetchTodos确实存在。

export const Todos = id => {
  const { todos, fetchTodos } = useContext(Context);
  useEffect(() => {
    fetchTodos()
  }, []);

  return (
    <div>
      <pre>{JSON.stringify(todos)}</pre>
    </div>
  );
};

最后,除非您确实需要从 树下的更多组件中使用此todos+fetchTodos组合(Todos您没有在问题中明确说明),否则我认为在不需要它们时使用 Context 会使问题复杂化。删除额外的间接层并useReducer直接在您的useTodos.

这里可能不是这种情况,但我发现人们在他们的脑海中混合了很多东西,并将一些简单的东西变成了一些复杂的东西(比如 Redux = Context + useReducer)。

希望能帮助到你!