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

How to update React Context from inside a child component?

我在上下文中的语言设置如下

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

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

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

export default LanguageProvider;

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

<LanguageProvider>
  <App>
    <MyPage />
  </App>
</LanguageProvider>

我的页面有一个组件可以切换语言

<MyPage>
  <LanguageSwitcher/>
</MyPage>

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;

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

使用钩子

Hooks 是在 16.8.0 中引入的,因此以下代码需要最低版本 16.8.0(向下滚动以查看 class 组件示例)。 CodeSandbox Demo

1。为动态上下文设置 parent 状态

首先,为了有一个可以传递给消费者的动态上下文,我将使用 parent 的状态。这确保了我有一个单一的真相来源。例如,我的 parent 应用程序将如下所示:

const App = () => {
  const [language, setLanguage] = useState("en");
  const value = { language, setLanguage };

  return (
    ...
  );
};

language 存储在状态中。稍后我们将通过上下文传递 language 和 setter 函数 setLanguage

2。创建上下文

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

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

这里我设置了 language ('en') 的默认值和一个 setLanguage 函数,它将由上下文提供者发送给消费者。这些只是默认值,我将在 parent App.

中使用提供程序组件时提供它们的值

注意:无论您使用钩子还是基于 class 的组件,LanguageContext 都保持不变。

3。创建上下文消费者

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

const LanguageSwitcher = () => {
  const { language, setLanguage } = useContext(LanguageContext);
  return (
    <button onClick={() => setLanguage("jp")}>
      Switch Language (Current: {language})
    </button>
  );
};

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

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

现在我将在 LanguageContext.Provider 中呈现我的语言切换器组件,并将必须通过上下文发送的值传递到更深的任何级别。这是我的 parent App 的样子:

const App = () => {
  const [language, setLanguage] = useState("en");
  const value = { language, setLanguage };

  return (
    <LanguageContext.Provider value={value}>
      <h2>Current Language: {language}</h2>
      <p>Click button to change to jp</p>
      <div>
        {/* Can be nested */}
        <LanguageSwitcher />
      </div>
    </LanguageContext.Provider>
  );
};

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

CodeSandbox Demo

使用 class 个组件

最新的context API was introduced in React 16.3 which provides a great way of having a dynamic context. The following code requires a minimum version of 16.3.0. CodeSandbox Demo

1。为动态上下文设置 parent 状态

首先,为了有一个可以传递给消费者的动态上下文,我将使用 parent 的状态。这确保了我有一个单一的真相来源。例如,我的 parent 应用程序将如下所示:

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

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

  ...
}

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

2。创建上下文

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

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

这里我设置了 language ('en') 的默认值和一个 setLanguage 函数,它将由上下文提供者发送给消费者。这些只是默认值,我将在 parent App.

中使用提供程序组件时提供它们的值

3。创建上下文消费者

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

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

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

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

现在我将在 LanguageContext.Provider 中呈现我的语言切换器组件,并将必须通过上下文发送的值传递到更深的任何级别。这是我的 parent 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>
    );
  }
}

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

CodeSandbox Demo

由于React推荐使用函数式组件和hooks所以我会用useContext和useState hooks来实现。以下是如何从子组件中更新上下文。

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

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

一个非常简单的解决方案是通过在您的提供程序中包含一个 setState 方法来在您的上下文中设置状态,如下所示:

return ( 
            <Context.Provider value={{
              state: this.state,
              updateLanguage: (returnVal) => {
                this.setState({
                  language: returnVal
                })
              }
            }}> 
              {this.props.children} 
            </Context.Provider>
        )

然后在您的消费者中,像这样调用 updateLanguage:

// button that sets language config
<Context.Consumer>
{(context) => 
  <button onClick={context.updateLanguage({language})}> 
    Set to {language} // if you have a dynamic val for language
  </button>
<Context.Consumer>

我个人喜欢这种模式:

文件: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
}

然后你可以创建一个功能组件,如果它是提供者树中的child:

文件:component.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>
    )
}

只是想补充一下 Divyanshu Maithani 的回答,即在将消费者包装在提供者中时使用 useMemo 通常更安全。

const App = () => {
  const [language, setLanguage] = useState("en");

  const value = useMemo(
    () => ({ language, setLanguage }),
    [language, setLanguage ],
  );

  return (
    <LanguageContext.Provider value={value}>
      <h2>Current Language: {language}</h2>
      <p>Click button to change to jp</p>
      <div>
        {/* Can be nested */}
        <LanguageSwitcher />
      </div>
    </LanguageContext.Provider>
  );
};

来自 react/jsx-no-constructed-context-values 规则:

React Context, and all its child nodes and Consumers are rerendered whenever the value prop changes. Because each Javascript object carries its own identity, things like object expressions ({foo: 'bar'}) or function expressions get a new identity on every run through the component. This makes the context think it has gotten a new object and can cause needless rerenders and unintended consequences.

This can be a pretty large performance hit because not only will it cause the context providers and consumers to rerender with all the elements in its subtree, the processing for the tree scan react does to render the provider and find consumers is also wasted.