【问题标题】:React New Context API - Access Existing Context across Multiple FilesReact New Context API - 跨多个文件访问现有上下文
【发布时间】:2018-03-21 12:40:14
【问题描述】:

我在 React 中看到的所有新的 Context API 示例都在一个文件中,例如https://github.com/wesbos/React-Context.

当我尝试让它跨多个文件工作时,我显然遗漏了一些东西。

我希望制作一个GlobalConfiguration 组件(下面的MyProvider)在上下文中创建和管理值,为从中读取的任何子组件(下面的MyConsumer)做好准备。

App.js

render() {
    return (
        <MyProvider>
            <MyConsumer />
        </MyProvider>
    );
}

provider.js

import React, { Component } from 'react';

const MyContext = React.createContext('test');

export default class MyProvider extends Component {

    render() {
        return (
            <MyContext.Provider
                value={{ somevalue: 1 }}>
                {this.props.children}
            </MyContext.Provider >
        );
    }
}

consumer.js

import React, { Component } from 'react';

const MyContext = React.createContext('test');

export default class MyConsumer extends Component {

    render() {

        return (
            <MyContext.Consumer>
                {(context) => (
                    <div>{context.state.somevalue}</div>
                )}
            </MyContext.Consumer>
        );
    }
}

不幸的是,在控制台中失败了:

consumer.js:12 Uncaught TypeError: Cannot read property 'somevalue' of undefined

我完全没有抓住重点吗?是否有文档或示例说明如何跨多个文件工作?

【问题讨论】:

  • 试试&lt;div&gt;{context.somevalue}&lt;/div&gt;
  • @Striped 在控制台中没有错误,但在context 中也没有。
  • 典型的错误文档,可能没有人会在单个文件中使用它,但可以在文档中使用它...

标签: reactjs


【解决方案1】:

我认为您遇到的问题是您正在创建两个不同的上下文,并试图将它们作为一个上下文使用。正是React.createContext创建的Context链接了ProviderConsumer

制作一个文件(我称之为configContext.js

configContext.js

import React, { Component, createContext } from "react";

// Provider and Consumer are connected through their "parent" context
const { Provider, Consumer } = createContext();

// Provider will be exported wrapped in ConfigProvider component.
class ConfigProvider extends Component {
  state = {
    userLoggedIn: false,                            // Mock login
    profile: {                                      // Mock user data
      username: "Morgan",
      image: "https://morganfillman.space/200/200",
      bio: "I'm Mogran—so... yeah."
    },
    toggleLogin: () => {
      const setTo = !this.state.userLoggedIn;
      this.setState({ userLoggedIn: setTo });
    }
  };

  render() {
    return (
      <Provider
        value={{
          userLoggedIn: this.state.userLoggedIn,
          profile: this.state.profile,
          toggleLogin: this.state.toggleLogin
        }}
      >
        {this.props.children}
      </Provider>
    );
  }
}

export { ConfigProvider };

// I make this default since it will probably be exported most often.
export default Consumer;

index.js

...
// We only import the ConfigProvider, not the Context, Provider, or Consumer.
import { ConfigProvider } from "./configContext";
import Header from "./Header";
import Profile from "./Profile";

import "./styles.css";

function App() {
  return (
    <div className="App">
      <ConfigProvider>
        <Header />
        <main>
          <Profile />
        </main>
        <footer>...</footer>
      </ConfigProvider>
    </div>
  );
}
...

Header.js

import React from 'react'
import LoginBtn from './LoginBtn'
... // a couple of styles
const Header = props => {
  return (
... // Opening tag, etc.
      <LoginBtn />  // LoginBtn has access to Context data, see file.
... // etc.
export default Header

LoginBtn.js

import React from "react";
import Consumer from "./configContext";

const LoginBtn = props => {
  return (
    <Consumer>
      {ctx => {
        return (
          <button className="login-btn" onClick={() => ctx.toggleLogin()}>
            {ctx.userLoggedIn ? "Logout" : "Login"}
          </button>
        );
      }}
    </Consumer>
  );
};

export default LoginBtn;

Profile.js

import React, { Fragment } from "react";
import Consumer from "./configContext"; // Always from that same file.

const UserProfile = props => {...}; // Dumb component

const Welcome = props => {...}; // Dumb component

const Profile = props => {
  return (
    <Consumer>
      ...
        {ctx.userLoggedIn ? (
          <UserProfile profile={ctx.profile} />
        ) : (<Welcome />)}
      ...
    </Consumer>
  ...

【讨论】:

  • 只是给道具。它应该已被接受。
【解决方案2】:

阅读 React-Context 的源代码,他们会这样做

<MyContext.Provider value={{
  state: this.state,
}}>

<MyContext.Consumer>
  {(context) => <p>{context.state.age}</p>}

如果你这样做了

<MyContext.Provider value={{ somevalue: 1 }}>
  {this.props.children}
</MyContext.Provider>

你应该得到somevalue这样的

<MyContext.Consumer>
  {(context) => <div>{context.somevalue}</div>}
</MyContext.Consumer>

编辑

如果您使用以下命令创建一个名为 myContext.js 的文件会怎样:

const MyContext = React.createContext('test');
export default MyContext;

然后像这样导入它:

import MyContext form '&lt;proper_path&gt;/myContext';

【讨论】:

  • 恐怕那是空白的。 myconsumer 似乎没有使用myprovider 中的上下文设置。有没有像 React.getContext() 这样的东西可以放在 consumer.js 中?
  • 我真的不知道...很抱歉,但是如果您不创建新上下文而是导入现有上下文会怎样。
  • 对不起,“导入现有的”是什么意思?
  • 这是否使 MyContext 有效地成为全局变量?我认为这与将 let SOME_VALUE = 1; export default SOME_VALUE; 放在 global.js 文件中并在任何地方导入它没有什么不同?
  • 它并不是真正的全局,因为它包含在您包含它的文件的范围内。但是,是的,它就像一个单例服务。
【解决方案3】:

到目前为止,即使名称相同,您在文件中创建的两个上下文也不相同。您需要导出您在其中一个文件中创建的上下文,并一直使用它。

这样的东西,在你的 provider.js 文件中:

import React, { Component } from 'react';

const MyContext = React.createContext();
export const MyContext;

export default class MyProvider extends Component {
    render() {
        return (
            <MyContext.Provider
                value={{ somevalue: 1 }}>
                {this.props.children}
            </MyContext.Provider >
        );
    }
}

然后在你的 consumer.js 文件中

import MyContext from 'provider.js';
import React, { Component } from 'react';
export default class MyConsumer extends Component {
    render() {
        return (
            <MyContext.Consumer>
                {(context) => (
                    <div>{context.somevalue}</div>
                )}
            </MyContext.Consumer>
        );
    }
}

【讨论】:

  • 你是对的,但@Striped 打败了你,所以我接受了他的回复。不过还是谢谢
  • 解析错误:标识符“MyContext”已被声明
  • 这是我最喜欢的答案。甜美而简单。
【解决方案4】:

我要把我的解决方案扔进锅里——它受到@Striped 的启发,只是将导出重命名为我认为有意义的东西。

import React, { Component } from 'react'
import Blockchain from './cloudComputing/Blockchain'

const { Provider, Consumer: ContextConsumer } = React.createContext()

class ContextProvider extends Component {
  constructor(props) {
    super(props)
    this.state = {
      blockchain: new Blockchain(),
    }
  }

  render() {
    return (
      <Provider value={this.state}>
        {this.props.children}
      </Provider>
    )
  }
}

module.exports = { ContextConsumer, ContextProvider }

现在很容易在任何组件中实现ContextConsumer

...
import { ContextConsumer } from '../Context'
...
export default class MyComponent extends PureComponent {
...
render() {
return (
  <ContextConsumer>
    {context => {
      return (
        <ScrollView style={blockStyle.scrollView}>
          {map(context.blockchain.chain, block => (
              <BlockCard data={block} />
          ))}
        </ScrollView>
      )
    }}
  </ContextConsumer>
)
}

我已经完成了 redux!

【讨论】:

    【解决方案5】:

    TLDR; Demo on CodeSandbox

    我目前解决相同问题的方法是使用Unstated library,它作为 React Context API 的便捷包装器。 “Unstated”还提供依赖注入,允许创建容器的离散实例;这对于代码重用和测试很方便。

    如何将 React/Unstated-Context 包装为服务

    以下骨架 API Service 包含 loggedIn 等状态属性以及两个服务方法:login()logout()。这些道具和方法现在可以在整个应用程序中使用,只需在每个需要上下文的文件中导入。

    例如:

    Api.js

    import React from "react";
    
    // Import helpers from Unstated 
    import { Provider, Subscribe, Container } from "unstated";
    
    // APIContainer holds shared/global state and methods
    class APIContainer extends Container {
      constructor() {
        super();
    
        // Shared props
        this.state = {
          loggedIn: false
        };
      }
    
      // Shared login method
      async login() {
        console.log("Logging in");
        this.setState({ loggedIn: true });
      }
    
      // Shared logout method
      async logout() {
        console.log("Logging out");
        this.setState({ loggedIn: false });
      }
    }
    
    // Instantiate the API Container
    const instance = new APIContainer();
    
    // Wrap the Provider
    const ApiProvider = props => {
      return <Provider inject={[instance]}>{props.children}</Provider>;
    };
    
    // Wrap the Subscriber
    const ApiSubscribe = props => {
      return <Subscribe to={[instance]}>{props.children}</Subscribe>;
    };
    
    // Export wrapped Provider and Subscriber
    export default {
        Provider: ApiProvider,
        Subscribe: ApiSubscribe
    }
    

    App.js

    现在Api.js 模块可以用作App.js 中的全局提供:

    import React from "React";
    import { render } from "react-dom";
    import Routes from "./Routes";
    import Api from "./Api";
    
    class App extends React.Component {
      render() {
        return (
          <div>
            <Api.Provider>
              <Routes />
            </Api.Provider>
          </div>
        );
      }
    }
    
    render(<App />, document.getElementById("root"));
    

    页面/Home.js:

    最后,Api.js 可以从 React 树的深处订阅 API 的状态。

    import React from "react";
    import Api from "../Api";
    
    const Home = () => {
      return (
        <Api.Subscribe>
          {api => (
            <div>
              <h1>? Home</h1>
              <pre>
                api.state.loggedIn = {api.state.loggedIn ? "? true" : "? false"}
              </pre>
              <button onClick={() => api.login()}>Login</button>
              <button onClick={() => api.logout()}>Logout</button>
            </div>
          )}
        </Api.Subscribe>
      );
    };
    
    export default Home;
    

    在此处尝试 CodeSandbox 演示:https://codesandbox.io/s/wqpr1o6w15

    希望有帮助!

    PS:如果我以错误的方式执行此操作,那么有人会快速打我的头。我很想学习不同/更好的方法。 - 谢谢!

    【讨论】:

      猜你喜欢
      • 2018-09-30
      • 2020-05-04
      • 2020-04-01
      • 2019-03-31
      • 1970-01-01
      • 2021-03-04
      • 2019-05-28
      • 2020-11-08
      相关资源
      最近更新 更多