为什么通过 useContext 使用另一个 hooks 值的自定义 hook 只显示其初始值?

Why does a custom hook that uses another hooks value via useContext only shows its initial value?

我正在尝试重用一堆自定义挂钩而不重新调用它们并且不维护我必须将级联参数从一个挂钩传递到另一个挂钩的顺序。

一个工作示例:https://codesandbox.io/s/laughing-firefly-mlhdw?file=/src/App.js:0-1158

给定以下代码:

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

const globalContext = React.createContext({
  user: null,
  pet: null
});

const usePet = () => {
  const [pet, setPet] = useState(null);
 
  useEffect(() => {
    setTimeout(() => {
      setPet("Dog");
    }, 3000);
  }, []);

  return pet;
};

const useUser = () => {
  const [user, setUser] = useState(null);
  // I want to proxy pet via the context so that I won't have to re-invoke its side-effects again
  const { pet } = useContext(globalContext);

  useEffect(() => {
    setTimeout(() => {
      setUser("john");
    }, 500);
  }, []);

  // This is only called once with the default value (null)
  useEffect(() => {
    console.log("Called from user!", { pet });
  }, [pet]);

  return user;
};

export const StateProvider = ({ children }) => {
  const user = useUser();
  const pet = usePet();

  useEffect(() => {
    console.log("StateProvider", { user, pet });
  }, [user, pet]);

  return (
    <globalContext.Provider value={{ user, pet }}>
      {children}
    </globalContext.Provider>
  );
};

export default function App() {
  const { user, pet } = useContext(globalContext);
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>
        {pet} {user}
      </h2>
    </div>
  );
}

// imagine an index.js that's wrapping the App component like this:
const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <StateProvider>
      <App />
    </StateProvider>
  </StrictMode>,
  rootElement
);

我希望在控制台输出中看到的内容如下


Called from user! {pet: null}
StateProvider {user: null, pet: null}
StateProvider {user: "john", pet: null}
StateProvider {user: "john", pet: "Dog"}
Called from user! {pet: "Dog"}

但我在 useUser 中没有得到任何更新,除了初始状态:

Called from user! {pet: null}
StateProvider {user: null, pet: null}
StateProvider {user: "john", pet: null}
StateProvider {user: "john", pet: "Dog"}
<!-- no update here, pet is still null for the useUser hook -->

我的问题是:

  1. 有可能实现吗?如果是,我在这里缺少什么?
  2. 如果不可能,是否有一种更优雅的方式在自定义挂钩之间传递数据而无需重新调用它们(为每次调用创建一个新的状态上下文)并且无需相互传递参数,这将迫使我还维持一切之间的秩序?

澄清一下 - UI 正在按预期工作,所有值都在组件内正确呈现。

还有,直接给hook传参的时候,事情也是井井有条的

const pet = usePet(); 
const user = useUser(pet); //this will work as it doesn't go through the context

回答你的问题:

1.是的,这是可以实现的。

这里缺少的是你的 useUser() 是在 StateProvider 组件内部调用的,即与上下文提供程序处于同一级别,而要使 useContext() 工作,它必须将 向下一级 称为上下文提供程序(StateProvider 作为包装器)。在这种情况下,它将是您的 App 组件。

工作代码如下:

export default function App() {
  const { user, pet } = useContext(globalContext);

  // this one will print out in console
  // Called from user!  {pet: "Dog"}
  // as expected
  const userWithInContext = useUser();

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>
        {pet} {user}
      </h2>
    </div>
  );
}

我测试了它,它起作用了。我没有做任何其他更改,只是使用您提供的相同 useUser() 逻辑在那里设置了一个新的 userWithInContext 变量。在控制台中,它将打印这些输出:

// this is from userWithInContext inside App component
Called from user! {pet: "Dog"}
// this is from user variable inside StateProvider component
Called from user! {pet: null}

2。因为有可能 实现你想要的,所以这只是关于 why you may not want to call useContext() inside a custom hook.

的优雅和可读性的旁注

要了解为什么会违反您的期望,您必须首先了解 context API works, including the useContext 钩子是如何实现的。

我将在此处包括一个特别相关的片段:

useContext

const value = useContext(MyContext);

Accepts a context object (the value returned from React.createContext) and returns the current context value for that context. The current context value is determined by the value prop of the nearest <MyContext.Provider> above the calling component in the tree.

当您使用上下文挂钩时,调用挂钩的组件仅在上下文更新时接收更新。 如果您在上下文提供程序树的根之外调用挂钩,将永远不会有更新。这就是您的示例代码中发生的情况。

一个简单的解决方案是简单地将依赖于上下文的挂钩调用移动到上下文提供程序根目录下的单独组件中。但是,由于您的自定义挂钩的方式 co-dependent(并且实际上都没有更新上下文本身),这仍然使您陷入循环依赖的僵局。

要解决这个问题,您必须让自己能够更新上下文:通过包含一个状态 setter,您可以使用值调用它来更新状态。

在您的代码的以下重构中,我进行了这两项更改:

This is a very common pattern, and the most common iteration of it is using the useReducer hook in combination with the context API. You can find lots of examples by querying for react state context.

<div id="root"></div><script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/standalone@7.17.2/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">

const {
  createContext,
  useContext,
  useEffect,
  useState,
} = React;

const initialContextState = {pet: null, user: null};

// You don't need to supply a default value here because you're no longer
// using it outside the provider root:
// const defaultContextValue = [initialContextState, () => {}];
// const appContext = createContext(defaultContextValue);

const appContext = createContext();

const usePet = () => {
  const [{pet}, setState] = useContext(appContext);
  useEffect(() => setTimeout(() => setState(state => ({...state, pet: 'Dog'})), 3000), [setState]);
  return pet;
};

const useUser = () => {
  const [{pet, user}, setState] = useContext(appContext);
  useEffect(() => setTimeout(() => setState(state => ({...state, user: 'John'})), 500), [setState]);
  useEffect(() => console.log('Called from user!', {pet}), [pet]);
  return user;
};

// This must be rendered within the context provider root
const ContextDependentHooksInvoker = () => {
  const pet = usePet();
  const user = useUser();

  useEffect(
    () => console.log('ContextDependentHooksInvoker', {pet, user}),
    [user, pet],
  );

  return null;
};

const StateProvider = ({children}) => {
  const stateWithSetter = useState(initialContextState);
  return (
    <appContext.Provider value={stateWithSetter}>
      <ContextDependentHooksInvoker />
      {children}
    </appContext.Provider>
  );
};

function App () {
  const [{pet, user}] = useContext(appContext);
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>{pet} {user}</h2>
    </div>
  );
}

function Example () {
  return (
    <React.StrictMode>
      <StateProvider>
        <App />
      </StateProvider>
    </React.StrictMode>
  );
}

ReactDOM.render(<Example />, document.getElementById('root'));

</script>