Доступ к контексту React за пределами функции рендеринга

93

Я разрабатываю новое приложение, используя новый React Context API вместо Redux, и раньше, Reduxкогда мне нужно было, например, получить список пользователей, я просто вызываю componentDidMountсвое действие, но теперь с React Context мои действия живут внутри my Consumer, который находится внутри моей функции рендеринга, а это означает, что каждый раз, когда вызывается моя функция рендеринга, она будет вызывать мое действие для получения списка пользователей, а это нехорошо, потому что я буду выполнять множество ненужных запросов.

Итак, как я могу вызвать свое действие только один раз, например, componentDidMountвместо вызова рендеринга?

Чтобы проиллюстрировать это, посмотрите на этот код:

Предположим, я оборачиваю все Providersв один компонент, например:

import React from 'react';

import UserProvider from './UserProvider';
import PostProvider from './PostProvider';

export default class Provider extends React.Component {
  render(){
    return(
      <UserProvider>
        <PostProvider>
          {this.props.children}
        </PostProvider>
      </UserProvider>
    )
  }
}

Затем я помещаю этот компонент Provider в оболочку всего моего приложения, например:

import React from 'react';
import Provider from './providers/Provider';
import { Router } from './Router';

export default class App extends React.Component {
  render() {
    const Component = Router();
    return(
      <Provider>
        <Component />
      </Provider>
    )
  }
}

Теперь, например, в представлении моих пользователей это будет примерно так:

import React from 'react';
import UserContext from '../contexts/UserContext';

export default class Users extends React.Component {
  render(){
    return(
      <UserContext.Consumer>
        {({getUsers, users}) => {
          getUsers();
          return(
            <h1>Users</h1>
            <ul>
              {users.map(user) => (
                <li>{user.name}</li>
              )}
            </ul>
          )
        }}
      </UserContext.Consumer>
    )
  }
}

Я хочу вот что:

import React from 'react';
import UserContext from '../contexts/UserContext';

export default class Users extends React.Component {
  componentDidMount(){
    this.props.getUsers();
  }

  render(){
    return(
      <UserContext.Consumer>
        {({users}) => {
          getUsers();
          return(
            <h1>Users</h1>
            <ul>
              {users.map(user) => (
                <li>{user.name}</li>
              )}
            </ul>
          )
        }}
      </UserContext.Consumer>
    )
  }
}

Но, конечно же, приведенный выше пример не работает, потому что getUsersон не находится в моих свойствах представления пользователей. Как правильно это сделать, если это вообще возможно?

Густаво Мендонса
источник

Ответы:

144

РЕДАКТИРОВАТЬ: с введением перехватчиков реакции в v16.8.0 вы можете использовать контекст в функциональных компонентах, используя useContextперехватчик

const Users = () => {
    const contextValue = useContext(UserContext);
    // rest logic here
}

РЕДАКТИРОВАТЬ: Начиная с версии 16.6.0 . Вы можете использовать контекст в методе жизненного цикла, используя this.contextкак

class Users extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* perform a side-effect at mount using the value of UserContext */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* render something based on the value of UserContext */
  }
}
Users.contextType = UserContext; // This part is important to access context values

До версии 16.6.0 это можно было сделать следующим образом

Чтобы использовать Context в своем методе жизненного цикла, вы должны написать свой компонент как

class Users extends React.Component {
  componentDidMount(){
    this.props.getUsers();
  }

  render(){
    const { users } = this.props;
    return(

            <h1>Users</h1>
            <ul>
              {users.map(user) => (
                <li>{user.name}</li>
              )}
            </ul>
    )
  }
}
export default props => ( <UserContext.Consumer>
        {({users, getUsers}) => {
           return <Users {...props} users={users} getUsers={getUsers} />
        }}
      </UserContext.Consumer>
)

Как правило, вы должны поддерживать один контекст в своем приложении, и имеет смысл упаковать вышеуказанный логин в HOC, чтобы использовать его повторно. Вы можете написать это как

import UserContext from 'path/to/UserContext';

const withUserContext = Component => {
  return props => {
    return (
      <UserContext.Consumer>
        {({users, getUsers}) => {
          return <Component {...props} users={users} getUsers={getUsers} />;
        }}
      </UserContext.Consumer>
    );
  };
};

а затем вы можете использовать его как

export default withUserContext(User);
Шубхам Хатри
источник
Это способ сделать это! Но я оборачиваю свои компоненты другими вещами, поэтому код будет становиться все сложнее и сложнее. Таким образом, использование библиотеки with-context и ее декораторов значительно упрощает код. Но спасибо за ответ!
Густаво Мендонса
У меня это не работает, я использую github with-context
PassionateDeveloper
1
Шубхам Хатри прав в своем ответе, есть небольшая опечатка, мешающая этому. Фигурные скобки предотвращают неявный возврат. Просто замените их скобками.
VDubs
1
Можете ли вы объяснить, как использовать this.contextв одном компоненте с несколькими контекстами? Предположим, у меня есть компонент, которому нужен доступ к UserContext и PostContext, как с этим справиться? Что я вижу, так это создание контекста, смешивающего оба, но это единственное или лучшее решение для этого?
Густаво Мендонса
1
@ GustavoMendonça Вы можете подписаться только на один контекст, используя реализацию API this.context. Если вам нужно прочитать более одного, вам нужно следовать шаблону рендеринга
Шубхам Хатри
3

Хорошо, я нашел способ сделать это с ограничением. С помощью with-contextбиблиотеки мне удалось вставить все данные о своих потребителях в свойства компонентов.

Но вставить более одного потребителя в один и тот же компонент сложно, вы должны создать смешанных потребителей с этой библиотекой, что делает код не элегантным и непродуктивным.

Ссылка на эту библиотеку: https://github.com/SunHuawei/with-context

РЕДАКТИРОВАТЬ: На самом деле вам не нужно использовать multi context api, который with-contextпредоставляет, на самом деле, вы можете использовать простой api и сделать декоратор для каждого вашего контекста, и если вы хотите использовать более одного потребителя в своем компоненте, просто объявите над своим классом столько декораторов, сколько захотите!

Густаво Мендонса
источник
1

С моей стороны этого было достаточно, чтобы добавить .bind(this)к событию. Так выглядит мой Компонент.

// Stores File
class RootStore {
   //...States, etc
}
const myRootContext = React.createContext(new RootStore())
export default myRootContext;


// In Component
class MyComp extends Component {
  static contextType = myRootContext;

  doSomething() {
   console.log()
  }

  render() {
    return <button onClick={this.doSomething.bind(this)}></button>
  }
}
Ballpin
источник
1
этот код выглядит намного проще, но на самом деле он вообще не обращается к значениям и не взаимодействует с экземпляром RootStore. Можете ли вы показать пример разделения на два файла и реактивных обновлений пользовательского интерфейса?
dcsan
-8

Вы должны передать контекст в более высокий родительский компонент, чтобы получить доступ в качестве реквизита в дочернем компоненте.

kprocks
источник