React New Context API - Доступ к существующему контексту в нескольких файлах

Все примеры, которые я видел в новом API контекста в React, находятся в одном файле, например 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

Я полностью упустил момент? Есть ли документация или пример того, как это работает в нескольких файлах?

Ответы

Ответ 1

Читая исходный код 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 '<proper_path>/myContext';

Ответ 2

Я думаю, что проблема, с которой вы сталкиваетесь, заключается в том, что вы создаете два разных контекста и пытаетесь использовать их как один. Это Context созданный React.createContext который связывает Provider и Consumer.

Создайте один файл (я назову его 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>
  ...

Ответ 3

На данный момент два контекста, которые вы создали в файлах, не совпадают, даже если они совпадают. Вам нужно экспортировать контекст, который вы создали в одном из файлов, и использовать его через out.

так что-то вроде этого, в файле 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>
        );
    }
}

Ответ 4

TL;DR; Демо на CodeSandbox

Мой текущий метод решения одной и той же проблемы заключается в использовании библиотеки Unstated, которая является удобной оболочкой API-интерфейса React Context. "Необязательный" также обеспечивает инъекцию зависимостей, позволяющую создавать отдельные экземпляры контейнера; что удобно для повторного использования и тестирования кода.

Как обернуть реакт/нестационарный контекст как услугу

Следующая служба API скелета содержит свойства состояния, такие как 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 может подписаться на состояние API из глубины дерева React.

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: Кто-то меня очень сильно избивает, если я делаю это неправильно. Мне бы хотелось узнать разные/лучшие подходы. - Спасибо!

Ответ 5

Я собираюсь бросить свое решение в банк - он был вдохновлен @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>
)
}

Я так делаю с редуксом!