【问题标题】:Access React Context outside of render function在渲染函数之外访问 React Context
【发布时间】:2018-09-23 09:26:56
【问题描述】:

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

那么,我如何才能只调用一次我的操作,例如在 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 不在我的用户视图道具中。如果可能的话,正确的做法是什么?

【问题讨论】:

    标签: reactjs react-context


    【解决方案1】:

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

    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 库和它的装饰器使代码更加简单。但是谢谢你的回答!
    • 它对我不起作用,我使用 with-context github
    • Shubham Khatri 的回答是正确的,只是一个小错字阻止了它的运行。花括号阻止了隐式返回。只需用括号替换它们。
    • 你能解释一下如何在同一个组件中使用this.context 和多个上下文吗?假设我有一个组件需要访问 UserContext 和 PostContext,如何处理呢?我能看到的是创建一个混合两者的上下文,但这是唯一或更好的解决方案?
    • @GustavoMendonça 您只能使用 this.context API 实现订阅单个上下文。如果您需要阅读多本,则需要遵循渲染道具模式
    【解决方案2】:

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

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

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

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

    【讨论】:

      【解决方案3】:

      就我而言,将.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 更新的示例?
      【解决方案4】:

      以下内容对我有用。这是一个使用 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();
      };
      

      【讨论】:

        【解决方案5】:

        您必须在更高的父组件中传递上下文才能在子组件中作为道具访问。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-08-22
          • 1970-01-01
          • 2018-09-26
          • 1970-01-01
          • 1970-01-01
          • 2019-03-05
          相关资源
          最近更新 更多