Acesse React Context fora da função de renderização

95

Estou desenvolvendo um novo aplicativo usando a nova API React Context em vez de Redux, e antes, com Redux, quando preciso obter uma lista de usuários, por exemplo, simplesmente chamo componentDidMountminha ação, mas agora com React Context, minhas ações ficam dentro my Consumer que está dentro da minha função de renderização, o que significa que toda vez que minha função de renderização for chamada, ela chamará minha ação para obter minha lista de usuários e isso não é bom porque estarei fazendo muitas solicitações desnecessárias.

Então, como posso chamar apenas uma vez minha ação, como em em componentDidMountvez de chamar em render?

Só para exemplificar, veja este código:

Vamos supor que estou envolvendo tudo Providersem um único componente, assim:

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

Então eu coloco esse componente Provider envolvendo todo o meu aplicativo, assim:

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

Agora, na minha visualização de usuários, por exemplo, será algo assim:

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

O que eu quero é isso:

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

Mas é claro que o exemplo acima não funciona porque os getUsersadereços não residem em meus usuários. Qual é a maneira certa de fazer isso, se isso for possível?

Gustavo Mendonça
fonte

Respostas:

148

EDIT: com a introdução de react -hooks na v16.8.0 , você pode usar o contexto em componentes funcionais fazendo uso de useContexthook

const Users = () => {
    const contextValue = useContext(UserContext);
    // rest logic here
}

EDITAR: A partir da versão 16.6.0 . Você pode fazer uso do contexto no método de ciclo de vida usando this.contextcomo

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

Antes da versão 16.6.0, você podia fazer isso da seguinte maneira

Para usar Context em seu método de ciclo de vida, você escreveria seu componente como

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

Geralmente, você manteria um contexto em seu aplicativo e faz sentido empacotar o login acima em um HOC para reutilizá-lo. Você pode escrever como

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

e então você pode usá-lo como

export default withUserContext(User);
Shubham Khatri
fonte
Essa é uma maneira de fazer isso! Mas estou envolvendo meus componentes com outras coisas, então o código ficará cada vez mais complicado fazendo dessa maneira. Portanto, usar a biblioteca with-context e seus decoradores torna o código muito mais simples. Mas obrigado pela resposta!
Gustavo Mendonça
Não funciona para mim, eu uso o github with-context
PassionateDeveloper
1
Shubham Khatri está correto em sua resposta, há apenas um pequeno erro de digitação impedindo que isso seja executado. As chaves estão evitando o retorno implícito. Basta substituí-los por parênteses.
VDubs
1
Você pode explicar como usar o this.contextcom vários contextos dentro do mesmo componente? Vamos supor que eu tenha um componente que precisa acessar UserContext e PostContext, como lidar com isso? O que posso ver é criar um contexto misturando os dois, mas essa é a única ou melhor solução para isso?
Gustavo Mendonça
1
@ GustavoMendonça Você só pode se inscrever em um único contexto usando a implementação da API this.context. Se você precisa ler mais de um, você precisa seguir o padrão de renderização
Shubham Khatri
3

Ok, encontrei uma maneira de fazer isso com uma limitação. Com owith-context biblioteca, consegui inserir todos os meus dados de consumidor em meus adereços de componente.

Mas, para inserir mais de um consumidor no mesmo componente é complicado de fazer, você tem que criar consumidores mistos com esta biblioteca, o que torna o código não elegante e improdutivo.

O link para esta biblioteca: https://github.com/SunHuawei/with-context

EDITAR: Na verdade, você não precisa usar a API multi-contexto que with-contextfornece, na verdade, você pode usar a API simples e fazer um decorador para cada um de seu contexto e se quiser usar mais de um consumidor em seu componente, basta declare acima de sua classe quantos decoradores você quiser!

Gustavo Mendonça
fonte
1

De minha parte, foi o suficiente para agregar .bind(this)ao evento. É assim que meu componente se parece.

// 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>
  }
}
Ballpin
fonte
1
esse código parece muito mais simples, mas na verdade não acessa valores ou interage com a instância do RootStore. você pode mostrar um exemplo dividido em dois arquivos e atualizações reativas da IU?
DCSAN
0

O seguinte está funcionando para mim. Este é um HOC que usa ganchos useContext e useReducer. Também há uma maneira de interagir com os soquetes neste exemplo.

Estou criando 2 contextos (um para despacho e outro para estado). Você precisaria primeiro envolver algum componente externo com o SampleProvider HOC. Então, usando uma ou mais funções de utilitário, você pode obter acesso ao estado e / ou ao despacho. O withSampleContexté bom porque passa tanto no despacho quanto no estado. Existem também outras funções como useSampleStateeuseSampleDispatch que podem ser usadas em um componente funcional.

Essa abordagem permite que você codifique seus componentes React como sempre fez, sem a necessidade de injetar qualquer sintaxe específica do Context.

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();
};
Hank
fonte
-1

2020+ com ganchos

Você pode acessar o contexto de hooks de reação fora de uma função de renderização, a única diferença é que seu componente não será renderizado novamente, mas pode ser propositalmente.

Em vez de fazer isso:

// store.js
import React from 'react';

const UserContext = React.createContext({
  user: {},
  setUser: () => null
});

export { UserContext };

Faça um fechamento:

// store.js
import React from 'react';

let user = {};
const UserContext = React.createContext({
  user,
  setUser: () => null
});

export { UserContext };

então você pode fazer

import { UserContext } from 'store';

console.log(UserContext._currentValue.user);

É isso aí!

Sebastien Horin
fonte
-8

Você tem que passar o contexto no componente pai superior para obter acesso como adereços na criança.

kprocks
fonte