如何从子组件内部更新React Context?

msh*_*eer 32 javascript reactjs

我在上下文中有语言设置,如下所示

class LanguageProvider extends Component {
  static childContextTypes = {
    langConfig: PropTypes.object,
  };

  getChildContext() {
    return { langConfig: 'en' };
  }

  render() {
    return this.props.children;
  }
}

export default LanguageProvider;
Run Code Online (Sandbox Code Playgroud)

我的应用程序代码如下所示

<LanguageProvider>
  <App>
    <MyPage />
  </App>
</LanguageProvider>
Run Code Online (Sandbox Code Playgroud)

我的页面有一个用于切换语言的组件

<MyPage>
  <LanguageSwitcher/>
</MyPage>
Run Code Online (Sandbox Code Playgroud)

LanguageSwitcher 在此MyPage需要更新上下文以将语言更改为'jp',如下所示

class LanguageSwitcher extends Component {
  static contextTypes = {
    langConfig: PropTypes.object,
  };

  updateLanguage() {
    //Here I need to update the langConfig to 'jp' 
  }

  render() {
    return <button onClick={this.updateLanguage}>Change Language</button>;
  }
}

export default LanguageSwitcher;
Run Code Online (Sandbox Code Playgroud)

如何从LanguageSwitcher组件内部更新上下文?

Div*_*ani 69

注意:我现在使用最新的上下文API,使用React 16.3,它提供了一种动态上下文的好方法.CodeSandbox演示

1.为动态上下文设置父状态

首先,为了获得可以传递给消费者的动态上下文,我将使用父级的状态.这确保我有一个单一的真理来源.例如,我的父App将如下所示:

class App extends Component {
  setLanguage = language => {
    this.setState({ language });
  };

  state = {
    language: "en",
    setLanguage: this.setLanguage
  };

  ...
}
Run Code Online (Sandbox Code Playgroud)

language与语言设置器方法一起存储在状态中,您可以将其保存在状态树之外.

2.创建上下文

接下来,我创建了一个这样的语言上下文:

// set the defaults
const LanguageContext = React.createContext({
  language: "en",
  setLanguage: () => {}
});
Run Code Online (Sandbox Code Playgroud)

这里我设置language('en')的默认值和一个setLanguage将由上下文提供程序发送给消费者的函数.这些只是默认值,我将在父级中使用提供程序组件时提供它们的值App.

3.创建上下文使用者

为了让语言切换器设置语言,它应该可以通过上下文访问语言设置器功能.它看起来像这样:

class LanguageSwitcher extends Component {
  render() {
    return (
      <LanguageContext.Consumer>
        {({ language, setLanguage }) => (
          <button onClick={() => setLanguage("jp")}>
            Switch Language (Current: {language})
          </button>
        )}
      </LanguageContext.Consumer>
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

在这里,我只是将语言设置为'jp',但您可能有自己的逻辑来为此设置语言.

4.将消费者包装在提供者中

现在我将把我的语言切换器组件渲染到一个LanguageContext.Provider并将必须通过上下文发送的值传递到更深层次的任何级别.这是我父母的App样子:

class App extends Component {
  setLanguage = language => {
    this.setState({ language });
  };

  state = {
    language: "en",
    setLanguage: this.setLanguage
  };

  render() {
    return (
      <LanguageContext.Provider value={this.state}>
        <h2>Current Language: {this.state.language}</h2>
        <p>Click button to change to jp</p>
        <div>
          {/* Can be nested */}
          <LanguageSwitcher />
        </div>
      </LanguageContext.Provider>
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

现在,只要单击语言切换器,它就会动态更新上下文.

CodeSandbox演示

  • 初始化上下文的默认值的目的是什么?这些默认值不是总是被“Provider”覆盖吗? (4认同)
  • 如果 setLanguage 没有参数,Typescript 会在我的情况下抱怨。`setLanguage: (language: string) =&gt; {}` 对我有用。 (4认同)
  • 对于使用 Typescript 的人来说,`setLanguage` 的类型是 `React.Dispatch&lt;React.SetStateAction&lt;string&gt;&gt;` (3认同)
  • 为什么上下文被限制为设置/获取一个简单的值......这似乎非常低效。一个更好的例子是突出显示具有默认对象的上下文并相应地更新该对象。 (2认同)
  • 这次真是万分感谢。这是最清晰的操作方法,但我并没有想拔掉自己的头发。 (2认同)
  • 很棒的答案。非常感谢您这样解释。 (2认同)

Mat*_*ani 23

Maithani的上述答案是一个很好的解决方案,但这是不使用钩子的类组件。由于React建议使用功能组件和挂钩,因此我将使用useContext和useState挂钩实现它。这是从子组件中更新上下文的方法。

LanguageContextMangement.js

import React, { useState } from 'react'

export const LanguageContext = React.createContext({
  language: "en",
  setLanguage: () => {}
})

export const LanguageContextProvider = (props) => {

  const setLanguage = (language) => {
    setState({...state, language: language})
  }

  const initState = {
    language: "en",
    setLanguage: setLanguage
  } 

  const [state, setState] = useState(initState)

  return (
    <LanguageContext.Provider value={state}>
      {props.children}
    </LanguageContext.Provider>
  )
}
Run Code Online (Sandbox Code Playgroud)

App.js

import React, { useContext } from 'react'
import { LanguageContextProvider, LanguageContext } from './LanguageContextManagement'

function App() {

  const state = useContext(LanguageContext)

  return (
    <LanguageContextProvider>
      <button onClick={() => state.setLanguage('pk')}>
        Current Language is: {state.language}
      </button>
    </LanguageContextProvider>
  )
}

export default App
Run Code Online (Sandbox Code Playgroud)

  • 我正在这样做,我的子组件中的 set 函数始终是我们在创建上下文时最初声明的函数: `() =&gt; {}` (3认同)
  • 我真的很喜欢您组织 LanguageContextMangement.js 文件的方式。在我看来,这是一种干净的做事方式,我现在就开始这样做。谢谢你! (3认同)
  • 需要澄清的是,在您的示例中,我认为state.setLanguage('pk')不会做任何事情,因为const state = useContext(LanguageContext)不在`LanguageContextProvider`之外。我通过将提供程序上移一个级别,然后在下一级的子级上使用`useContext`来解决了我的问题。 (2认同)
  • 如果您不想将上下文提供者向上移动一级,也可以使用上下文使用者,如下所示:`&lt;LanguageContext.Consumer&gt; {value =&gt; /*在这里访问您的值*/}&lt;/LanguageContext.Consumer&gt;` 。 (2认同)
  • 感谢您的认可,这真的鼓励我继续从事这样的工作! (2认同)
  • 不幸的是,这对我不起作用,当我尝试使用子组件中的上下文时,“setState”不起作用。 (2认同)

Nic*_*ton 20

我个人喜欢这种模式:

文件:context.jsx

import React from 'react';

// The Context 
const TemplateContext = React.createContext();

// Template Provider
const TemplateProvider = ({children}) => {

    const [myValue, setMyValue] = React.useState(0);

    // Context values passed to consumer
    const value = {
        myValue,    // <------ Expose Value to Consumer
        setMyValue  // <------ Expose Setter to Consumer
    };

    return (
        <TemplateContext.Provider value={value}>
            {children}
        </TemplateContext.Provider>
    )
}

// Template Consumer
const TemplateConsumer = ({children}) => {
    return (
        <TemplateContext.Consumer>
            {(context) => {
                if (context === undefined) {
                    throw new Error('TemplateConsumer must be used within TemplateProvider');
                }
                return children(context)
            }}
        </TemplateContext.Consumer>
    )
}

// useTemplate Hook
const useTemplate = () => {
    const context = React.useContext(TemplateContext);
    if(context === undefined)
        throw new Error('useTemplate must be used within TemplateProvider');
    return context;
}

export {
    TemplateProvider,
    TemplateConsumer,
    useTemplate
}
Run Code Online (Sandbox Code Playgroud)

然后您可以创建一个功能组件(如果它是提供程序树中的子组件):

文件:组件.jsx

import React            from 'react';
import {useTemplate}    from 'context.jsx';
const MyComponent = () => {

    // Get the value and setter from the consumer hook
    const {myValue, setMyValue} = useTemplate();

    // Demonstrate incrementing the value
    React.useEffect(() => {

        // Increment, set in context
        const increment = () => setMyValue(prev => prev + 1); 

        // Increment every second
        let interval = setInterval(increment, 1000);

        // Cleanup, kill interval when unmounted
        return () => clearInterval(interval);

    },[]) // On mount, no dependencies

    // Render the value as it is pulled from the context
    return (
        <React.Fragment>
            Value of MyValue is: {myValue}
        </React.Fragment>
    )
}
Run Code Online (Sandbox Code Playgroud)


Luc*_*uez 7

这是我的方法,基于@Nicholas Hamilton 的答案,但适用于 TypeScript 并应用@LIIT 建议

注意:我正在使用 Next.js 和高度固执己见的 ESLint 设置。

import { createContext, useContext, useMemo, useState, Dispatch, SetStateAction } from "react"

interface TemplateContextProps {
  myValue: number | null
  setMyValue: Dispatch<SetStateAction<number | null>>
}

const TemplateContext = createContext<TemplateContextProps>({
  myValue: null,
  setMyValue: (prevState: SetStateAction<number | null>) => prevState,
})

interface TemplateProviderProps {
  children: React.ReactNode
}

function TemplateProvider({ children }: TemplateProviderProps): JSX.Element {
  const [myValue, setMyValue] = useState<number | null>(null)

  const value = useMemo(() => ({ myValue, setMyValue }), [myValue, setMyValue])

  return <TemplateContext.Provider value={value}>{children}</TemplateContext.Provider>
}

const TemplateConsumer = TemplateContext.Consumer

const useTemplate = () => useContext(TemplateContext)

export { TemplateProvider, TemplateConsumer, useTemplate }
Run Code Online (Sandbox Code Playgroud)

我喜欢将值初始化为 null,这是一种更动态的方法,但您可以将类型限制为仅数字并将其默认设置为 0。