Access React Context outside of render function

I am developing a new app using the new React Context API instead of Redux, and before, with Redux, when I needed to get a list of users for example, I simply call in componentDidMount my action, but now with React Context, my actions live inside my Consumer which is inside my render function, which means that every time my render function is called, it will call my action to get my users list and that is not good because I will be doing a lot of unecessary requests.

So, how I can call only one time my action, like in componentDidMount instead of calling in render?

Just to exemplify, look at this code:

Let's suppose that I am wrapping all my Providers in one component, like this:

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>
)
}
}

Then I put this Provider component wrapping all my app, like this:

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>
)
}
}

Now, at my users view for example, it will be something like this:

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>
)
}
}

What I want is this:

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>
)
}
}

But ofcourse that the example above don't work because the getUsers don't live in my Users view props. What is the right way to do it if this is possible at all?

121728 次浏览

好吧,我找到了一个有限制的方法。使用 with-context库,我设法将所有消费者数据插入到组件道具中。

但是,要在同一个组件中插入多个使用者是一件复杂的事情,您必须使用这个库创建混合使用者,这使得代码不够优雅,也不具有生产效率。

到这个库的链接: https://github.com/SunHuawei/with-context

编辑: 实际上你不需要使用 with-context提供的多上下文 api,事实上,你可以使用简单的 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);

You have to pass context in higher parent component to get access as a props in child.

对于我来说,添加 .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>
}
}

下面是我的工作。这是一个 HOC,使用 useContext 和 useReducerhooks

我正在创建两个上下文(一个用于分派,一个用于状态)。首先需要用 SampleProviderHOC 包装一些外部组件。然后,通过使用一个或多个实用程序函数,您可以获得对状态和/或分派的访问权。withSampleContext非常好,因为它同时通过了分派和状态。还有其他函数,如 useSampleStateuseSampleDispatch,可以在功能组件中使用。

这种方法允许您像往常一样编写 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.log(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.log('Broker reported error: ' + frame.headers['message']);
console.log('Additional details: ' + frame.body);
};
client.activate();
};