如何将 React Hooks Context 与提供者的多个值一起使用

IT技术 reactjs react-hooks
2021-05-04 22:10:01

在 React 中共享一些全局值和函数的最佳方式是什么?

现在我有一个 ContextProvider ,里面有所有这些:

<AllContext.Provider
  value={{
    setProfile, // second function that changes profile object using useState to false or updated value
    profileReload, // function that triggers fetch profile object from server
    deviceTheme, // object
    setDeviceTheme, // second function that changes theme object using useState to false or updated value
    clickEvent, // click event
    usePopup, // second function of useState that trigers some popup
    popup, // Just pass this to usePopup component
    windowSize, // manyUpdates on resize (like 30 a sec, but maybe can debounce)
   windowScroll // manyUpdates on resize (like 30 a sec, but maybe can debounce)
  }}
>

但是就像文档中的悲伤一样:因为上下文使用引用标识来确定何时重新渲染,所以当提供者的父级重新渲染时,有一些问题可能会在消费者中触发意外渲染。例如,下面的代码将在每次 Provider 重新渲染时重新渲染所有消费者,因为总是为 value 创建一个新对象:

这不好:

<Provider value={{something: 'something'}}>

还行吧:

this.state = {
      value: {something: 'something'},
    };
<Provider value={this.state.value}>

我想将来我可能会有多达 30 个上下文提供程序,而且它不是很友好:/

那么如何将这个全局值和函数传递给组件呢?我只能

  1. 为所有内容创建单独的 contextProvider。
  2. 将一些一起使用的东西,如配置文件及其功能、主题及其功能(参考标识怎么样?)
  3. 也许组只是因为不改变自己而起作用?那么参考身份呢?)
  4. 其他最简单的方法?

我在 Provider 中使用的示例:

// Resize
  const [windowSize, windowSizeSet] = useState({
    innerWidth: window.innerWidth,
    innerHeight: window.innerHeight
  })
// profileReload
const profileReload = async () => {
    let profileData = await fetch('/profile')
    profileData = await profileData.json()

    if (profileData.error)
      return usePopup({ type: 'error', message: profileData.error })

    if (localStorage.getItem('deviceTheme')) {
      setDeviceTheme(JSON.parse(localStorage.getItem('deviceTheme'))) 
    } else if (profileData.theme) {
      setDeviceTheme(JSON.parse(JSON.stringify(profileData.theme)))
    } else {
      setDeviceTheme(settings.defaultTheme) 
    }
    setProfile(profileData)
  }

// Click event for menu close if clicked outside somewhere and other
const [clickEvent, setClickEvent] = useState(false)
const handleClick = event => {
  setClickEvent(event)
}
// Or in some component user can change theme just like that
setDeviceTheme({color: red})
4个回答

将什么组合在一起的主要考虑(从性能的角度来看)不是关于哪些一起使用,而更多关于哪些一起改变对于大多数情况下(或至少很少)设置到上下文中的事物,您可能可以将它们全部放在一起而不会出现任何问题。但是如果有一些更频繁地混合在这种变化中的东西,可能值得将它们分开。

例如,deviceTheme对于给定的用户,我希望它是相当静态的,并且可能被大量组件使用。我猜这popup可能是在管理有关您当前是否打开了弹出窗口的某些信息,因此它可能会随着与打开/关闭弹出窗口相关的每个操作而改变。如果popupdeviceTheme捆绑在同一个上下文中,那么每次popup更改都会导致所有依赖的组件deviceTheme也重新渲染。所以我可能会有一个单独的PopupContext. windowSize并且windowScroll可能有类似的问题。什么确切的使用方法可以深入到意见领域,但是您可以AppContext为不经常变化的部分提供一个更具体的上下文,然后为更频繁变化的事物提供更具体的上下文。

下面的 CodeSandbox 演示了 useState 和 useContext 之间的交互,上下文分为几种不同的方式和一些按钮来更新上下文中保存的状态。

编辑 zk58011yol

您可以转到此 URL以在完整的浏览器窗口中查看结果。我鼓励您首先了解结果的工作原理,然后查看代码并在您想了解其他场景时对其进行试验。

这个答案已经很好地解释了如何构建上下文以提高效率。但最终目标是让上下文消费者仅在需要时更新。这取决于具体的情况,最好使用单个还是多个上下文。

在这一点上,这个问题对于大多数全局状态 React 实现来说是常见的,例如 Redux。一个常见的解决方案是仅在需要时使用React.PureComponent,React.memoshouldComponentUpdatehook使消费者组件更新

const SomeComponent = memo(({ theme }) => <div>{theme}</div>);

...

<AllContext>
  {({ deviceTheme }) => <SomeComponent theme={deviceTheme}/>
</AllContext>

SomeComponent将仅在deviceTheme更新时重新渲染,即使上下文或父组件已更新。这可能是也可能不是可取的。

Ryan 的回答很棒,您应该在设计如何构建上下文提供程序层次结构时考虑这一点。

我提出了一个解决方案,您可以使用该解决方案更新 provider 中的多个值,并具有多个useStates

例子 :

const TestingContext = createContext()


const TestingComponent = () => {
    const {data, setData} = useContext(TestingContext)
    const {value1} = data
    return (
        <div>
            {value1} is here
            <button onClick={() => setData('value1', 'newline value')}>
                Change value 1
            </button>
        </div>
    )
}

const App = () => {
    const values = {
        value1: 'testing1',
        value2: 'testing1',
        value3: 'testing1',
        value4: 'testing1',
        value5: 'testing1',
    }

    const [data, setData] = useState(values)

    const changeValues = (property, value) => {
        setData({
            ...data,
            [property]: value
        })
    }

    return (
        <TestingContext.Provider value={{data, setData: changeValues}}>
            <TestingComponent/>
            {/* more components here which want to have access to these values and want to change them*/}
        </TestingContext.Provider>
    )
    }

您仍然可以将它们组合起来!如果您关心性能,可以更早地创建对象。我不知道您使用的值是否会发生变化,如果没有变化,这很容易:

state = {
  allContextValue: {
    setProfile,
    profileReload,
    deviceTheme,
    setDeviceTheme,
    clickEvent,
    usePopup,
    popup,
    windowSize
  }
}

render() {
  return <AllContext.Provider value={this.state.allContextValue}>...</AllContext>;
}

每当您想要更新您需要执行的任何值时,我都喜欢这样做,但是:

this.setState({
  allContextValue: {
    ...this.state.allContextValue,
    usePopup: true,
  },
});

这将既高效又相对容易:) 将它们拆分可能会加快一点,但我只会在您发现它实际上很慢时才这样做,并且仅适用于您的上下文中具有很多消费者。

不过,如果你的value没有太大变化,那就真的没有什么可担心的。