在渲染函数之外访问 React Context
Access React Context outside of render function
我正在使用新的 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
库,我设法将我所有的消费者数据插入到我的组件道具中。
但是,要将多个消费者插入到同一个组件中是一件很复杂的事情,您必须使用此库创建混合消费者,这使得代码不优雅且效率低下。
本库的link:https://github.com/SunHuawei/with-context
编辑:实际上你不需要使用 with-context
提供的多上下文 api,事实上,你可以使用简单的 api 并为每个上下文制作一个装饰器你的上下文,如果你想在你的组件中使用多个消费者,只需在你的 class 上面声明你想要的装饰器!
编辑: 随着 v16.8.0 中 react-hooks 的引入,您可以通过使用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);
您必须在更高的父组件中传递上下文才能在子组件中作为道具访问。
就我而言,将 .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>
}
}
以下为我工作。这是一个使用 useContext 和 useReducer 挂钩的 HOC。 在此示例中还有一种与套接字交互的方法。
我正在创建 2 个上下文(一个用于调度,一个用于状态)。您首先需要使用 SampleProvider HOC 包装一些外部组件。然后通过使用一个或多个实用函数,您可以访问状态 and/or 调度。 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.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();
};
我正在使用新的 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
库,我设法将我所有的消费者数据插入到我的组件道具中。
但是,要将多个消费者插入到同一个组件中是一件很复杂的事情,您必须使用此库创建混合消费者,这使得代码不优雅且效率低下。
本库的link:https://github.com/SunHuawei/with-context
编辑:实际上你不需要使用 with-context
提供的多上下文 api,事实上,你可以使用简单的 api 并为每个上下文制作一个装饰器你的上下文,如果你想在你的组件中使用多个消费者,只需在你的 class 上面声明你想要的装饰器!
编辑: 随着 v16.8.0 中 react-hooks 的引入,您可以通过使用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);
您必须在更高的父组件中传递上下文才能在子组件中作为道具访问。
就我而言,将 .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>
}
}
以下为我工作。这是一个使用 useContext 和 useReducer 挂钩的 HOC。 在此示例中还有一种与套接字交互的方法。
我正在创建 2 个上下文(一个用于调度,一个用于状态)。您首先需要使用 SampleProvider HOC 包装一些外部组件。然后通过使用一个或多个实用函数,您可以访问状态 and/or 调度。 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.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();
};