如何使用 React Hooks Context 为提供者提供多个值

How to use React Hooks Context with multiple values for Providers

在 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)
  }}
>

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

这很糟糕:

<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})

你仍然可以组合它们!如果您关心性能,您可以更早地创建对象。我不知道你使用的值是否改变,如果他们不改变那很容易:

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,
  },
});

这将既高效又相对简单:) 将它们分开可能会加快一点,但我只会在你发现它实际上很慢时才这样做,并且只针对你的上下文中会有很多消费者的部分。

不过,如果你的值变化不大的话,真的没什么好担心的。

将什么组合在一起的主要考虑因素(从性能的角度来看)不是关于哪些一起使用,而是更多关于哪些 change 一起使用。对于大部分设置到上下文中一次(或至少很少)的事物,您可以毫无问题地将它们全部放在一起。但是,如果其中混合了一些更频繁更改的内容,则可能值得将它们分开。

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

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

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

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

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

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

...

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

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

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

我想出了一个解决方案,您可以使用它来更新具有多个 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>
    )
    }

根据 Koushik 的回答,我制作了自己的打字稿版本。

import React from "react"

type TestingContextType = {
  value1?: string,
  value2?: string,
  value3?: string,
  value4?: string,
  value5?: string,
}

const contextDefaultValues = {
  data: { 
    value1: 'testing1',
    value2: 'testing1',
    value3: 'testing1',
    value4: 'testing1',
    value5: 'testing1'
  } as TestingContextType,
  setData: (state: TestingContextType) => {}
};

const TestingContext = React.createContext(contextDefaultValues);

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

const App = () => {
  const [data, setData] = React.useState(contextDefaultValues.data)
  const changeValues = (value : TestingContextType) => setData(data && 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>
  )
}