Accede a React Context fuera de la función de renderizado

93

Estoy desarrollando una nueva aplicación usando la nueva API de React Context en lugar de Redux, y antes, Reduxcuando necesitaba obtener una lista de usuarios, por ejemplo, simplemente llamo a componentDidMountmi acción, pero ahora con React Context, mis acciones viven dentro my Consumer que está dentro de mi función de renderizado, lo que significa que cada vez que se llame a mi función de renderizado, llamará a mi acción para obtener mi lista de usuarios y eso no es bueno porque haré muchas solicitudes innecesarias.

Entonces, ¿cómo puedo llamar solo una vez a mi acción, como en en componentDidMountlugar de llamar a render?

Solo para ejemplificar, mire este código:

Supongamos que estoy envolviendo todo mi Providersen un componente, así:

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

Luego puse este componente de proveedor envolviendo toda mi aplicación, así:

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

Ahora, en la vista de mis usuarios, por ejemplo, será algo como esto:

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

Lo que quiero es esto:

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

Pero, por supuesto, el ejemplo anterior no funciona porque getUsersno viven en los accesorios de vista de mis usuarios. ¿Cuál es la forma correcta de hacerlo si es posible?

Gustavo Mendonça
fuente

Respuestas:

144

EDITAR: Con la introducción de react-hooks en v16.8.0 , puede usar el contexto en componentes funcionales haciendo uso de useContexthook

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

EDITAR: Desde la versión 16.6.0 en adelante. Puede hacer uso del contexto en el método del ciclo de vida usando this.contextlike

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 de la versión 16.6.0, podía hacerlo de la siguiente manera

Para usar Context en su método de ciclo de vida, escribiría su 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>
)

En general, mantendría un contexto en su aplicación y tiene sentido empaquetar el inicio de sesión anterior en un HOC para reutilizarlo. Puedes escribirlo 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>
    );
  };
};

y luego puedes usarlo como

export default withUserContext(User);
Shubham Khatri
fuente
¡Esa es una forma de hacerlo! Pero estoy envolviendo mis componentes con otras cosas, por lo que el código se volverá cada vez más complicado de esta manera. Entonces, usar la biblioteca con contexto y sus decoradores hace que el código sea mucho más simple. ¡Pero gracias por la respuesta!
Gustavo Mendonça
No funciona para mí, uso el github con contexto
PassionateDeveloper
1
Shubham Khatri tiene razón en su respuesta, solo hay un pequeño error tipográfico que impide que esto se ejecute. Las llaves están impidiendo el retorno implícito. Simplemente reemplácelos con paréntesis.
VDubs
1
¿Puede explicar cómo usar el this.contextcon múltiples contextos dentro del mismo componente? Supongamos que tengo un componente que necesita acceder a UserContext y PostContext, ¿cómo manejar eso? Lo que puedo ver es crear un contexto mezclando ambos, pero ¿esa es la única o mejor solución para esto?
Gustavo Mendonça
1
@ GustavoMendonça Solo puede suscribirse a un único contexto utilizando la implementación de la API de this.context. Si necesita leer más de uno, debe seguir el patrón de apoyo de renderizado
Shubham Khatri
3

Ok, encontré una manera de hacer esto con una limitación. Con la with-contextbiblioteca logré insertar todos los datos de mi consumidor en los accesorios de mis componentes.

Pero, para insertar más de un consumidor en el mismo componente es complicado de hacer, hay que crear consumidores mixtos con esta biblioteca, lo que hace que el código no sea elegante ni productivo.

El enlace a esta biblioteca: https://github.com/SunHuawei/with-context

EDITAR: En realidad, no necesita usar la API de contexto múltiple que with-contextproporciona, de hecho, puede usar la API simple y hacer un decorador para cada uno de sus contextos y si desea usar más de un consumidor en su componente, solo ¡declara por encima de tu clase tantos decoradores como quieras!

Gustavo Mendonça
fuente
1

Por mi parte fue suficiente agregar .bind(this)al evento. Así es como se ve mi componente.

// 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
fuente
1
este código parece mucho más simple, pero en realidad no accede a los valores ni interactúa con la instancia de RootStore en absoluto. ¿Puede mostrar un ejemplo dividido en dos archivos y actualizaciones de IU reactivas?
dcsan
-8

Debe pasar el contexto en el componente principal superior para obtener acceso como accesorios en el niño.

kprocks
fuente