Todo lo que acabas de ver en este artículo es solo una pequeña parte del libro Aplicaciones reactivas con React, NodeJS & MongoDB, El libro más completo en español para aprender a crear aplicaciones web completas con las tecnologías más potentes de la actualidad, desde el Frontend con React, hasta el Backend con un poderoso API REST con NodeJS y Express y persistiendo todo en MongoDB. te invito a que veas mi libro:
Ver libroReact Context
Uno de los principales problemas de trabajar con React, es que para poder llevar datos globales de la aplicación a todos los componentes, es necesario replicar estos valores por toda la estructura de la aplicación mediante props, de tal forma que los componentes padres deben de replicar estos valores a sus hijos y así sucesivamente.
Para analizar mejor esta problemática, analicemos la imagen anterior, donde se puede ver que el componente A sabe cuál es el usuario autenticado en la aplicación, luego, este debe de ser propagado a diversos componentes en la estructura. El problema con esto es que, absolutamente todos los componentes en la jerarquía deben recibir y a su vez propagar el usuario a sus hijos, ya que de lo contrario, no habrá forma de tener una referencia a él.
Otro de los problemas es que muchas veces estas propiedades no son requeridas por todos los componentes, pero solo el hecho de que alguno de sus descendientes lo necesite, implica modificar toda la cadena de componentes hacia arriba para recibir y replicar los valores globales.
Para solucionar este tipo de problemas, tenemos el Context, un nuevo API agregado a partir de la versión 16.3.0, la cual nos permite pasar datos a través del árbol de componentes sin tener que pasar props manualmente en cada nivel.
Si bien, el Context nos permite compartir cualquier datos a toda la aplicación, no fue diseñado para utilizarse arbitrariamente para pasar lo que sea, en su lugar, se utiliza únicamente para compartir datos que se consideren “globales”, es decir, datos que pueden ser necesario en varios componentes de la aplicación sin importar el nivel en la jerarquía en que se encuentre, como puede ser el usuario autenticado, el tema, idioma o incluso formatos de fecha o moneda predefinidos por el usuario.
createContext
El primer paso para utilizar el Context es crear uno, para esto, React proporciona el método createContext
, que recibe como parámetro el valor por default del dato global o que queremos compartir en toda la aplicación. Veamos un ejemplo de cómo sería:
1import React from 'react'
2import Image from 'next/image'
3
4 const context = React.createContext( {
5 val1: "val1",
6 val2: "val2"
7 } )
8export default context
Obseva que la función createContext
puede recibir un valor, el cual pertenece al valor por default del contexto al momento de iniciarse, sin embargo, en muchas ocasiones no es necesario mandarle nada, pues no podemos definir un valor por default hasta no ir al API para recupera los datos, en estos casos, simplemente podemos crear el contexto sin un valor por default.
Algo importante a tomar en cuenta es que, el contexto deberá ser creado como un archivo independiente, pues eso permite que lo podamos referencias desde cualquier parte, lo que hace que sea independiente de la estructura de componentes.
Podríamos decir que en este punto estamos como en la imagen anterior, donde tenemos un contexto (azul) independiente de la estructura y con un valor inicial por default, sin embargo, podemos ver que el contexto aún está aislado de la aplicación.
Una aplicación puede tener más de un Context, donde cada uno puede guardar valores globales independientes, por lo que podemos crear diferentes Context con createContext.
Provider
Lo siguiente por implementar es el Provider
, un componente que viene con cada Context
y que permite que los componentes se suscriban a los cambios en el Context
, permitiendo detectar los cambios y renderizarse nuevamente para reflejar los cambios.
El componente Provider viene por defecto en cada Context
, por lo que para crearlo, lo hacemos atreves de la referencia del contexto que creamos previamente, y recibirá como parámetro la propiedad value
, la cual se utiliza para establecer el valor actual del Context
. Veamos un ejemplo:
1import React from 'react'
2import Image from 'next/image'
3import Context from './context/Context'
4
5class MyComponent extends React.Component {
6
7 constructor(props) {
8 super(props)
9 this.state = {
10 ...
11 }
12 }
13
14 componentDidMount() {
15 ...
16 setState({
17 //new state
18 })
19 }
20
21 render() {
22 return (
23 <Context.Provider value={this.state}>
24 <MyApp />
25 </Context.Provider>
26 )
27 }
28}
29export default MyComponent
Lo primero que tenemos que observar es que para crear el Provider
, es necesario importar el Contexto (línea 2) que creamos en la sección pasada con el método createContext
, una vez con la referencia, podemos ver que creamos el Provider por medio de la referencia al Context
(línea 22).
Lo segundo interesante es la propiedad value
, la cual la utilizamos para indicarle al Provider el nuevo valor del Context, en este caso, observa que le asignamos el valor actual del estado. Algo importante a considerar es que al momento de establecer el value
, estamos sobrescribiendo el valor por default que establecimos al momento de crear el contexto con createContext
.
En este punto podemos decir que estamos como en la imagen anterior, donde el Context ya está creado y estamos sincronizando el valor del Context
por medio del estado del componente. De esta forma, cada vez que cambie el estado del componente, el Provider
tomará este nuevo estado como el nuevo valor del contexto.
Hay que tomar en cuenta que, así como podemos tener múltiples Context
, podemos tener múltiples Providers
, por lo que solo habría que anidarlos de la siguiente manera:
1import React from 'react'
2import Image from 'next/image'
3import UserContext from './context/UserContext'
4import ThemeContext from './context/ThemeContext'
5
6class MyComponent extends React.Component {
7
8render() {
9 return (
10 <UserContext.Provider value={this.state.user}>
11 <ThemeContext.Provider value={this.state.theme}>
12 <MyApp />
13 </ThemeContext.Provider>
14 </UserContext.Provider>
15 )
16}
17}
18export default MyComponent
Observa que cada Provider
se crear por medio de la referencia al Context
correspondiente, además, cada uno puede definir el valor de la propiedad value
de diferentes fuentes.
Consumer
El último paso es crear el Consumer
, el cual es un componente provisto también por el Context
, que permite suscribirse a los cambios del Context
.
Como regla, los Consumer
deberán ser descendientes en cualquier nivel del Provider
, ya que será este último quien notifique los cambios del contexto al Consumer
.
Para crear un Consumer
también deberemos utilizar la referencia al Context
y deberá de tener como hijo una función:
1import React from 'react'
2import Image from 'next/image'
3import Context from './context/Context'
4
5class MyChildComponent extends React.Component {
6
7 render() {
8 return (
9 <Context.Consumer>
10 {context =>
11 <p>{context.value}</p>
12 }
13 </Context.Consumer>
14 )
15 }
16}
17export default MyChildComponent
La sintaxis del Consumer puede llegar a ser un poco confusa la primera vez, pero verás que ahora que la expliquemos será más fácil. Lo primero que tenemos que ver es que el Consumer
se crear por medio del Context
y no requiere de ninguna propiedad. En segundo lugar, podrás ver que el Consumer
no recibe un componente directamente como hijo, sino más bien un arrow function, el cual recibe como parámetro el valor actual del context, además deberá de retornar un elemento renderizable.
El arrow function que recibe el Consumer
será ejecutado cada vez que el Provider
actualice el valor del contexto mediante la propiedad value
, de esta forma, todos los componentes se podrán actualizar con el nuevo valor del Context
, pero con la única diferencia de que cuando el Contexto es actualizado, el método shouldComponentUpdate
es ignorado.
Finalmente, cabe mencionar que de la misma forma que podemos tener múltiples Context y Providers, también podemos anidar múltiples Consumers para recuperar el valor de múltiples Contexts. Veamos un ejemplo:
1import React from 'react'
2import Image from 'next/image'
3import UserContext from './context/UserContext'
4import ThemeContext from './context/ThemeContext'
5
6class MyComponent extends React.Component {
7
8 render() {
9 return (
10 <UserContext.Consumer>
11 {user =>
12 <ThemeContext.Consumer>
13 {theme =>
14 <p>{user.name}</p>
15 <p>{theme.color}</p>
16 }
17 </ThemeContext.Consumer value={}>
18 }
19 </UserContext.Consumer>
20 )
21 }
22}
23export default MyComponent
En este punto, se podrá decir que estamos como en la imagen siguiente:
En esta nueva imagen podemos ver que todo el ciclo se cierra, pues ya tenemos el Context
creado, el Provider
está siendo actualizado y los componentes hijos están recibiendo las actualizaciones del contexto por medio de los Consumers.
contextType
Hace un momento veíamos como suscribirnos al contexto mediante el componente Consumer
, pero también nos dimos cuenta que la sintaxis es algo rebuscada, por tal motivo, React ofrece una segunda forma de obtener el contexto con una sintaxis más simple, que es mediante contextType
.
Antes de continuar, debo de resaltar dos limitantes de este nuevo método, y es que solo funciona en compontes de clase, es decir que extienden de React.Component
y la segunda es que solo nos permite utilizar un Context
a la vez, de tal forma que si nuestro componente es de función (sin estado) o requieres utilizar más de un contexto a la vez, no podremos utilizarlo.
Para implementar contextType
veamos el siguiente ejemplo:
1import React from 'react'
2import Image from 'next/image'
3import Context from './context/Context'
4
5class MyComponent extends React.Component {
6
7 render() {
8 return (
9 <p>{this.context.name}</p>
10 )
11 }
12}
13
14MyComponent.contextType = Context
15
16export default MyComponent
Analicemos las partes de este componente. Primero que nada, tendremos que importar el Context
como ya lo veníamos haciendo, luego, tendremos que asignar el contexto a la propiedad contextType
de la clase (línea 13). Observa que esta asignación se hace a nivel de clase y fuera de la declaración de la misma. Esto hace que el contexto esté disponible en la propiedad this.context
, tal y como podemos ver que lo utilizamos en la línea 8.