Skip to content

Cómo utilizar el contexto en React

Una de las tareas más extenuantes en React es pasar parámetros entre componentes, especialmente si dichos componentes se encuentran varios niveles debajo del componente padre que está enviando los parámetros. Agregar un parámetro implica actualizar toda la cadena hasta llegar al componente destino. Esto sin embargo cambió con la introducción de la API de contexto de React, que no es más que una implementación nativa de lo que anteriormente se lograba mediante librerías como Redux y Mobx.

Usualmente tenemos en un render una cadena de componentes que se ve algo como esto:

<ComponentePadre>
    <ComponenteNivel1>
    </ComponenteNivel1>
</ComponentePadre>

Pero se da la casualidad que dentro de ComponenteNivel1 tenemos un segundo componente anidado:

<ComponenteNivel1>
    <ComponenteNivel2>
    </ComponenteNivel2>
</ComponenteNivel1>

Si por alguna razón necesitamos que el componente en el nivel 2 acceda a algún dato que se encudentra en el componente padre, es necesario conectarlo de forma manual:

{/* /src/padre.jsx */}
<ComponentePadre>
    <ComponenteNivel1
        algunValor={this.state.algunValor}
        otroValor={this.state.otroValor}>
    </ComponenteNivel1>
</ComponentePadre>

{/* /src/nivel1.jsx */}
<ComponenteNivel1>
    <ComponenteNivel2
        algunValor={this.props.algunValor}
        otroValor={this.props.otroValor}>
</ComponenteNivel2>

De forma simplificada acá lo que está sucediendo es que las variables originales se encuentran en el estado de nuestro hipotético componente padre (padre.jsx) y para hacerlos llegar hasta el componente que se encuentra anidado en nuestro también hipotético componente en el nivel 1 es necesario conectar la cadena manualmente. En el componente padre tomamos el valor del estado y ya en el componente que únicamente está siguiendo la cadena está pasando los datos como parámetros recibidos. Esto si bien es perfectamente funcional tiene la desventaja de que cualquier cambio, llámese agregar un nuevo valor, implica agregar dicho valor en toda la cadena desde el origen hasta el destino. Además de poco elegante se convierte en una auténtica pesadilla en aplicativos medianamente complejos.

¿Por qué necesitamos una API de contexto?

La API de contexto de React ha existido desde hace ya algún tiempo pero no era seguro utilizarla puesto que aún estaba en etapa experimental. Es por ello que siempre han existido librerías de terceros como las ya mencionadas al inicio para manejar un contexto de forma segura. No fue sino hasta la versión 16.03 de React que ya la API fue lo suficientemente estable como para considerarla lista para su uso en productivo.

Ahora bien, el contexto en un aplicativo de React no es sencillamente un mecanismo para pasar parámetros entre componentes. Se trata más bien de una forma de unificar el almacenamiento de estado en una aplicación de React, básicamente convirtiendo dicho contexto en una especie de variable global que nos permite consumir esa información desde cualquier componente de la aplicación. Esto definitivamente es una forma más elegante y segura de mantener comunicación entre componentes mediante el acceso a un estado global que estar pasando manualmente variables y funciones en cascada entre componentes.

Implementación de un contexto

Existen varias técnicas para construir un contexto y poder pasarlo entre componentes en un aplicativo React. En este ejemplo haremos un wrapper que expondrá nuestro contexto de forma que pueda ser consumido como una API.

import React, { Component } from 'react';

const MiContexto = React.createContext();
export class MiContextoProvider extends Component {
    constructor(props) {
        super(props);
        this.state = {
            variableUno: "",
            variableDos: ""
        };
    };

    funcionUno = (valor) => {
        this.setState({
            variableUno: valor
        });
    };

    funcionDos = (valor) => {
        this.setState({
            variableDos: valor
        });
    };

    render() {
        const { children } = this.props;

        <MiContexto.Provider
            value={{
                variableUno: this.state.variableUno,
                variableDos: this.state.variableDos,
                funcionUno: this.funcionUno,
                funcionDos: this.funcionDos
            }}>
            {children}
        </MiContexto.Provider>
    };
};

export const MiContextoConsumer = MiContexto.Consumer;
export default MiContexto;

Acá están sucediendo varias cosas, sin embargo notemos que se trata de un componente estándar de React. Previo a declarar nuestro componente estamos creando explícitamente un contexto en la línea 3. La mayor parte del contenido de este componente sigue la estructura tradicional de un componente en cuanto al manejo del estado y funciones internas, sin embargo lo interesante sucede en el render a partir de la línea 28: acá estamos utilizando el Provider del contexto que definimos previamente y estamos explícitamente asociando los valores de dicho provider al estado y funciones internas de nuestro componente. Cada vez que algún componente en nuestro aplicativo consuma este provider estará accediendo al mismo contexto que cualquier otro componente que también esté haciendo uso del mismo, convirtiendo prácticamente el estado y funciones de este componente en variables globales.

No menos importante es lo que sucede en la línea 40: estamos proveyendo un Consumer para que otros componentes puedan consumir este contexto. Esta parte es importantísima. Ahora veamos que sucede cuando un componente necesita hacer uso de este contexto.

Consumo desde un componente estándar

import React, { Component } from 'react';
import MiContexto from '/ruta/hacia/mi-contexto';

class MiComponente extends Component {
    static contextType = MiContexto;

    constructor(props) {
        super(props);

        // Contenido del constructor
    };

    miFuncionQueUtilizaContexto = () => {
        this.setState({
            valorCualquiera: this.context.variableUno 
        });
    };

    miFuncionQueUtilizaContextoTambien = (valor) => {
        this.setState({
            valorCualquiera: valor
        }, () => {
            this.context.funcionUno(this.state.valorCualquiera);
        });
    };

    // Resto del componente
};

export default MiComponente;

En este caso estamos haciendo uso del contexto que hemos creado dentro de un componente estándar de React. Este componente tiene su propio estado y sus propias funciones pero puede acceder al contexto global.

Importamos el contexto en la línea 2 pero la magia sucede en la línea 5. Esa pequeña declaración hace que cuando en la línea 15 hagamos uso de this.context nuestro controlador entienda que el contexto al que estamos haciendo referencia aquí es el mismo que importamos desde nuestro archivo original. Adicionalmente en la línea 23 estamos invocando una de las funciones de nuestro contexto para mutar una de las variables de nuestro contexto. Esto hace que cualquier otro componente consumiendo nuestro contexto vea reflejados los cambios a dichas variables también mediante el uso de funciones adecuadas para acceder y modificar dichos valores.

Consumo desde componente funcional

import React from 'react';
import { MiContextoConsumer } from '/ruta/hacia/mi-contexto';

const MiComponenteFuncional = () => {
    return (
        <MiContextoConsumer>
            {({ variableUno, variableDos, funcionUno, funcionDos }) => (
                <>
                    Contenido de variable uno: {variableUno}
                    <Button
                        onClick={funcionUno}>
                    </Button>
                </>
            )}
        </MiContextoConsumer>
    );
};

export default MiComponenteFuncional;

Cuando utilizamos componentes funcionales (es decir, un componente sin estado) utilizamos el consumer que declaramos en nuestro archivo de contexto original. Este consumer expone también las mismas variables y funciones del contexto pero debe ser previamente inicializado en nuestro componente funcional tal y como se muestra en las líneas 6 a la 15.

Conclusión

La API de contexto de React es una herramienta muy útil pero que puede complicarse muy rápidamente si no entendemos claramente el concepto de cómo funciona adecuadamente. Si bien facilita enormemente la comunicación entre módulos debe limitarse su uso para casos muy puntuales puesto que mezclar distintos contextos dentro de un mismo aplicativo nuevamente nos puede llevar a una situación de complejidad innecesaria.

Published inTutorial