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





Хорошо, я нашел способ сделать это с ограничением. С помощью библиотеки 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);
У меня не работает, я использую github с контекстом
В документации по контекстному API говорится, что «[Вы можете ссылаться на [this.context] в любом из методов жизненного цикла] (responsejs.org/docs/context.html#classcontexttype)», но вы подразумеваете, что это неправда? Кажется, что ваш код прилагает некоторые усилия, чтобы снова ввести контекст в реквизиты, а не там, где это должно быть API.
Шубхам Хатри прав в своем ответе, есть небольшая опечатка, мешающая этому. Фигурные скобки предотвращают неявный возврат. Просто замените их скобками.
Можете ли вы объяснить, как использовать this.context с несколькими контекстами внутри одного и того же компонента? Предположим, у меня есть компонент, которому нужен доступ к UserContext и PostContext, как с этим справиться? Что я вижу, так это создание контекста, смешивающего оба, но это единственное или лучшее решение для этого?
@ GustavoMendonça Вы можете подписаться только на один контекст, используя реализацию API this.context. Если вам нужно прочитать более одного, вам нужно следовать шаблону рендеринга.
Понятно, спасибо за ответ! Надеюсь, они предоставят способ использовать несколько контекстов, используя только this.context.
вы также можете объявить его в классе как статический, то есть: static contextType = UserContext;
Вы должны передать контекст в более высокий родительский компонент, чтобы получить доступ в качестве реквизита в дочернем компоненте.
С моей стороны было достаточно добавить на мероприятие .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. Можете ли вы показать пример разделения на два файла и реактивных обновлений пользовательского интерфейса?
Следующее работает для меня. Это 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();
};
Это способ сделать это! Но я оборачиваю свои компоненты другими вещами, поэтому код будет становиться все сложнее и сложнее. Таким образом, использование библиотеки with-context и ее декораторов значительно упрощает код. Но спасибо за ответ!