在渲染函数之外访问 React Context

IT技术 reactjs react-context
2021-04-07 00:48:21

我正在使用新的 React Context API 而不是 Redux 开发一个新的应用程序,之前使用Redux,当我需要获取用户列表时,我只需调用componentDidMount我的操作,但现在使用 React Context,我的操作就在里面我的消费者在我的渲染函数中,这意味着每次调用我的渲染函数时,它都会调用我的操作来获取我的用户列表,这并不好,因为我会做很多不必要的请求。

那么,我如何才能只调用一次我的动作,比如 incomponentDidMount而不是调用 render?

只是为了举例,看看这段代码:

假设我将所有内容都包装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它不在我的用户视图中。如果可能的话,正确的方法是什么?

5个回答

编辑:随着v16.8.0中 react-hooks 的引入,您可以通过使用useContexthook在功能组件中使用上下文

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);
Shubham Khatri 在他的回答中是正确的,只是有一个小错误阻止了它的运行。花括号阻止了隐式返回。只需将它们替换为括号即可。
2021-05-23 00:48:21
@GustavoMendonça 您只能使用 this.context API 实现订阅单个上下文。如果您需要阅读多个,则需要遵循渲染道具模式
2021-05-26 00:48:21
你能解释一下如何this.context在同一个组件中使用多个上下文吗?假设我有一个需要访问 UserContext 和 PostContext 的组件,如何处理?我能看到的是创建一个混合两者的上下文,但这是唯一或更好的解决方案吗?
2021-06-02 00:48:21
它对我不起作用,我使用带上下文的 github
2021-06-05 00:48:21
这是一种方法!但是我正在用其他东西包装我的组件,所以代码会变得越来越复杂。因此,使用 with-context 库及其装饰器使代码更加简单。但是谢谢你的回答!
2021-06-10 00:48:21

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

但是,在同一个组件中插入多个消费者是很复杂的,你必须用这个库创建混合消费者,这使得代码不优雅并且没有生产力。

本库链接:https : //github.com/SunHuawei/with-context

编辑:实际上,您不需要使用提供的多上下文 api,with-context事实上,您可以使用简单的 api 并为您的每个上下文制作一个装饰器,如果您想在组件中使用多个消费者,只需在您的class之上声明尽可能多的装饰器!

就我而言,添加.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>
  }
}
这段代码看起来简单得多,但它实际上根本不访问值或与 RootStore 实例交互。你能展示一个分成两个文件和反应式 UI 更新的例子吗?
2021-06-03 00:48:21

以下是为我工作。这是一个使用 useContext 和 useReducer 钩子的 HOC。 在这个例子中还有一种与套接字交互的方法。

我正在创建 2 个上下文(一个用于调度,一个用于状态)。您首先需要使用 SampleProvider HOC 包装一些外部组件。然后通过使用一个或多个效用函数,您可以访问状态和/或调度。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();
};

您必须在更高的父组件中传递上下文才能作为子组件中的props获得访问权限。