NodersJS
Published in

NodersJS

Manejo de estado con Context y Hooks en React

Un flotador en una piscina con mucho contraste
Foto por Timothy Meinberg en Unsplash.
import React from 'react';const AlbumOfTheWeek = React.createContext({
title: 'Pop Food',
artist: 'Jack Stauber',
genre: 'Edible Pop', // lol
});
export default AlbumOfTheWeek;
import React from 'react';
import ReactDOM from 'react-dom';
import AlbumOfTheWeek from './context/album-of-the-week';function App() {
return (
<UserProfile />
);
}
function UserProfile() {
return (
<section>
<h1>Hi I'm Osman and this is my album of the week:</h1>
<AlbumOfTheWeek.Consumer>
{album => (
<dl>
<dt>Title:</dt>
<dd>{album.title}</dd>
<dt>Artist:</dt>
<dd>{album.artist}</dd>
<dt>Genre:</dt>
<dd>{album.genre}</dd>
</dl>
)}
</AlbumOfTheWeek.Consumer>
</section>
);
}
ReactDOM.render(
<App />,
document.getElementByID('root')
);
// ...
import { getAlbumOfTheWeek } from './services/album-of-the-week';
class App extends React.Component {
state = {
album: null
};
componentDidMount() {
getAlbumOfTheWeek().then(res => {
this.setState({ album: res.data });
});
}
render() {
return (
<AlbumOfTheWeek.Provider value={this.state.album}>
<UserProfile />
</AlbumOfTheWeek.Provider>
);
}
}
function UserProfile() {
return (
<section>
<h1>Hi I'm Osman and this is my album of the week:</h1>
<AlbumOfTheWeek.Consumer>
// Renderizamos la información sólo si album es truthy
{album => album && (
<dl>
<dt>Title:</dt>
<dd>{album.title}</dd>
<dt>Artist:</dt>
<dd>{album.artist}</dd>
<dt>Genre:</dt>
<dd>{album.genre}</dd>
</dl>
)}
</AlbumOfTheWeek.Consumer>
</section>
);
}

Zapatero a tus zapatos — pastelero a tus pasteles.

Hasta el momento nuestro componente App se encarga de implementar la lógica para obtener la información del álbum de la semana y de renderizar UserProfile. Podríamos ir un paso más adelante y separar completamente estas dos responsabilidades:

class AlbumOfTheWeekProvider extends React.Component {
state = {
album: null
};
componentDidMount() {
getAlbumOfTheWeek().then(res => {
this.setState({ album: res.data });
});
}
render() {
const { children } = this.props;
return (
<AlbumOfTheWeek.Provider value={this.state.album}>
{children}
</AlbumOfTheWeek.Provider>
);
}
}
function App() {
return (
<AlbumOfTheWeekProvider>
<UserProfile />
</AlbumOfTheWeekProvider>
);
}
ReactDOM.render(
<App />,
document.getElementByID('root')
);

Introduciendo Hooks.

Una de las desventajas de utilizar render props es que es muy difícil utilizar la data de nuestro contexto fuera del método render (o fuera de la función que le pasamos de hijo al consumidor). Digamos que ahora queremos implementar client-side routing en nuestra aplicación y al entrar a la página del álbum de la semana, queremos actualizar el título del documento con el nombre del artista y del álbum. Actualizamos nuestro componente App para implementar el enrutamiento utilizando react-router:

import React from "react";
import {
BrowserRouter as Router, Switch, Route, NavLink
} from "react-router-dom";
// ...function App() {
return (
<AlbumOfTheWeekProvider>
<Router>
<nav>
<NavLink exact activeClassName="active" to="/">
Home
</NavLink>
<NavLink activeClassName="active" to="/album-of-the-week">
Album
</NavLink>
</nav>
<Switch>
<Route exact path="/" component={Home} />
<Route
path="/album-of-the-week"
component={UserProfile}
/>
</Switch>
</Router>
</AlbumOfTheWeekProvider>
);
}
class UserProfile extends React.Component {
static contextType = AlbumOfTheWeek;
componentDidMount() {
// ¿Actualizamos document.title aquí?
if (this.context) {
document.title = this.context.title;
}
}
componentDidUpdate() {
// ¿O lo actualizamos aquí?
if (document.title !== this.context.title) {
document.title = this.context.title;
}
}
render() {
return (
<section>
<h1>Hi I'm Osman and this is my album of the week:</h1>
{this.context && (
<dl>
<dt>Title:</dt>
<dd>{this.context.title}</dd>
<dt>Artist:</dt>
<dd>{this.context.artist}</dd>
<dt>Genre:</dt>
<dd>{this.context.genre}</dd>
</dl>
)}
</section>
);
}
}
function UserProfile() {
const album = React.useContext(AlbumOfTheWeek);
React.useEffect(() => {
if (album) {
document.title = album.title;
}
}, [album]);
return (
<section>
<h1>Hi I'm Osman and this is my album of the week:</h1>
{album && (
<dl>
<dt>Title:</dt>
<dd>{album.title}</dd>
<dt>Artist:</dt>
<dd>{album.artist}</dd>
<dt>Genre:</dt>
<dd>{album.genre}</dd>
</dl>
)}
</section>
);
}

Un caso de confianza.

El lector ávido (🧐 😂) habrá notado que para efectos de nuestra aplicación no tiene mucho sentido hacer el llamado a getAlbumOfTheWeek dentro de AlbumOfTheWeekProvider cuando entramos en la ruta / y cargamos el componente Home, pues dentro de este no tenemos necesidad de acceder al contexto. O tal vez sí creemos que es necesario cargarlo con antelación para tener la información ya disponible cuando naveguemos a /album-of-the-week. Dicho eso ¿Qué aproximación es mejor? ¿Cómo decido qué hacer?

Confía en el corazón de las cartas

No! NO! Cualquier cosa menos el corazón de las cartas!!!

Cambiar de lugar donde se provee el contexto.

O en otras palabras, mover AlbumOfTheWeekProvider dentro de la ruta /album-of-the-week:

function App() {
return (
<Router>
<nav>
<NavLink exact activeClassName="active" to="/">
Home
</NavLink>
<NavLink activeClassName="active" to="/album-of-the-week">
Album
</NavLink>
</nav>
<Switch>
<Route exact path="/" component={Home} />
<Route
path="/album-of-the-week"
render={routeProps => (
<AlbumOfTheWeekProvider>
<UserProfile {...routeProps} />
</AlbumOfTheWeekProvider>
)}
/>
</Switch>
</Router>
);
}

Contexto como una API.

Otra alternativa es delegar la responsabilidad de hacer el llamado a getAlbumOfTheWeek, exponiendo en AlbumOfTheWeekProvider una API para que cualquier consumidor del contexto pueda iniciarlo:

class AlbumOfTheWeekProvider extends React.Component {
state = {
album: null,
getAlbumOfTheWeek: this.getAlbumOfTheWeek.bind(this)
};
getAlbumOfTheWeek() {
if (this.state.album) {
return;
}

return getAlbumOfTheWeek().then(res => {
this.setState({ album: res.data });
});
};
render() {
const { children } = this.props;
return (
<AlbumOfTheWeek.Provider value={this.state}>
{children}
</AlbumOfTheWeek.Provider>;
);
}
}
function UserProfile() {
const {
album, getAlbumOfTheWeek
} = React.useContext(AlbumOfTheWeek);
useEffect(() => {
if (album) {
document.title = album.title;
}
}, [album]);
// Añadimos este efecto
useEffect(() => {
getAlbumOfTheWeek();
}, [getAlbumOfTheWeek]);
return (
<section>
<h1>Hi I'm Osman and this is my album of the week:</h1>
{album && (
<dl>
<dt>Title:</dt>
<dd>{album.title}</dd>
<dt>Artist:</dt>
<dd>{album.artist}</dd>
<dt>Genre:</dt>
<dd>{album.genre}</dd>
</dl>
)}
</section>
);
}

Conclusión.

Finalmente no existe una regla de oro para implementar tus contextos y probablemente te hayan surgido algunas dudas sobre como organizar tu estado — de hecho, desde release de hooks, han salido muchas librerías de manejo de estado con contexto y, la comunidad ha encontrado diversos patrones alrededor de este tema.

--

--

¿Por qué? Porque nos gusta.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store