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

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

Итак, как я могу вызвать свое действие только один раз, как в 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 не находится в моих свойствах просмотра пользователей. Как правильно это сделать, если это вообще возможно?

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
102
0
88 003
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

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

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

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

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

Ответ принят как подходящий

Обновлено: С введением перехватчиков реакции в 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 и ее декораторов значительно упрощает код. Но спасибо за ответ!

Gustavo Mendonça 13.04.2018 18:16

У меня не работает, я использую github с контекстом

PassionateDeveloper 26.06.2018 21:18

В документации по контекстному API говорится, что «[Вы можете ссылаться на [this.context] в любом из методов жизненного цикла] (responsejs.org/docs/context.html#classcontexttype)», но вы подразумеваете, что это неправда? Кажется, что ваш код прилагает некоторые усилия, чтобы снова ввести контекст в реквизиты, а не там, где это должно быть API.

bignose 24.10.2018 13:20

Шубхам Хатри прав в своем ответе, есть небольшая опечатка, мешающая этому. Фигурные скобки предотвращают неявный возврат. Просто замените их скобками.

VDubs 26.09.2018 16:36

Можете ли вы объяснить, как использовать this.context с несколькими контекстами внутри одного и того же компонента? Предположим, у меня есть компонент, которому нужен доступ к UserContext и PostContext, как с этим справиться? Что я вижу, так это создание контекста, смешивающего оба, но это единственное или лучшее решение для этого?

Gustavo Mendonça 16.01.2019 21:31

@ GustavoMendonça Вы можете подписаться только на один контекст, используя реализацию API this.context. Если вам нужно прочитать более одного, вам нужно следовать шаблону рендеринга.

Shubham Khatri 17.01.2019 06:30

Понятно, спасибо за ответ! Надеюсь, они предоставят способ использовать несколько контекстов, используя только this.context.

Gustavo Mendonça 18.01.2019 02:08

вы также можете объявить его в классе как статический, то есть: static contextType = UserContext;

jymbob 11.02.2020 21:50

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

С моей стороны было достаточно добавить на мероприятие .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.info()
  }

  render() {
    return <button onClick = {this.doSomething.bind(this)}></button>
  }
}

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

dcsan 02.11.2019 18:29

Следующее работает для меня. Это HOC, который использует хуки useContext и useReducer. В этом примере также есть способ взаимодействия с сокетами.

Я создаю 2 контекста (один для отправки и один для состояния). Сначала вам нужно обернуть некоторый внешний компонент с помощью SampleProvider HOC. Затем, используя одну или несколько служебных функций, вы можете получить доступ к состоянию и / или диспетчеру. withSampleContext хорош тем, что передает и отправку, и состояние. Есть также другие функции, такие как useSampleState и useSampleDispatch, которые можно использовать в функциональном компоненте.

Этот подход позволяет вам кодировать ваши компоненты React, как всегда, без необходимости вводить какой-либо синтаксис, специфичный для контекста.

import React, { useEffect, useReducer } from 'react';
import { Client } from '@stomp/stompjs';
import * as SockJS from 'sockjs-client';

const initialState = {
  myList: [],
  myObject: {}
};

export const SampleStateContext = React.createContext(initialState);
export const SampleDispatchContext = React.createContext(null);

const ACTION_TYPE = {
  SET_MY_LIST: 'SET_MY_LIST',
  SET_MY_OBJECT: 'SET_MY_OBJECT'
};

const sampleReducer = (state, action) => {
  switch (action.type) {
    case ACTION_TYPE.SET_MY_LIST:
      return {
        ...state,
        myList: action.myList
      };
    case ACTION_TYPE.SET_MY_OBJECT:
      return {
        ...state,
        myObject: action.myObject
      };
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
};

/**
 * Provider wrapper that also initializes reducer and socket communication
 * @param children
 * @constructor
 */
export const SampleProvider = ({ children }: any) => {
  const [state, dispatch] = useReducer(sampleReducer, initialState);
  useEffect(() => initializeSocket(dispatch), [initializeSocket]);

  return (
    <SampleStateContext.Provider value = {state}>
      <SampleDispatchContext.Provider value = {dispatch}>{children}</SampleDispatchContext.Provider>
    </SampleStateContext.Provider>
  );
};

/**
 * HOC function used to wrap component with both state and dispatch contexts
 * @param Component
 */
export const withSampleContext = Component => {
  return props => {
    return (
      <SampleDispatchContext.Consumer>
        {dispatch => (
          <SampleStateContext.Consumer>
            {contexts => <Component {...props} {...contexts} dispatch = {dispatch} />}
          </SampleStateContext.Consumer>
        )}
      </SampleDispatchContext.Consumer>
    );
  };
};

/**
 * Use this within a react functional component if you want state
 */
export const useSampleState = () => {
  const context = React.useContext(SampleStateContext);
  if (context === undefined) {
    throw new Error('useSampleState must be used within a SampleProvider');
  }
  return context;
};

/**
 * Use this within a react functional component if you want the dispatch
 */
export const useSampleDispatch = () => {
  const context = React.useContext(SampleDispatchContext);
  if (context === undefined) {
    throw new Error('useSampleDispatch must be used within a SampleProvider');
  }
  return context;
};

/**
 * Sample function that can be imported to set state via dispatch
 * @param dispatch
 * @param obj
 */
export const setMyObject = async (dispatch, obj) => {
  dispatch({ type: ACTION_TYPE.SET_MY_OBJECT, myObject: obj });
};

/**
 * Initialize socket and any subscribers
 * @param dispatch
 */
const initializeSocket = dispatch => {
  const client = new Client({
    brokerURL: 'ws://path-to-socket:port',
    debug: function (str) {
      console.info(str);
    },
    reconnectDelay: 5000,
    heartbeatIncoming: 4000,
    heartbeatOutgoing: 4000
  });

  // Fallback code for http(s)
  if (typeof WebSocket !== 'function') {
    client.webSocketFactory = function () {
      return new SockJS('https://path-to-socket:port');
    };
  }

  const onMessage = msg => {
    dispatch({ type: ACTION_TYPE.SET_MY_LIST, myList: JSON.parse(msg.body) });
  };

  client.onConnect = function (frame) {
    client.subscribe('/topic/someTopic', onMessage);
  };

  client.onStompError = function (frame) {
    console.info('Broker reported error: ' + frame.headers['message']);
    console.info('Additional details: ' + frame.body);
  };
  client.activate();
};

Другие вопросы по теме