Reemplazar Redux con React Context Hook
July 27, 2019
En este post quiero hablar sobre el uso del Hook useContext de React para usarlo en lugar de Redux ya que en algunas situaciones no deseamos agregar un valor a la Store es allí donde Context API entra en escena.
React useContext
const value = useContext(MyContext)
El siguiente ejemplo lo desarollaremos usando create-react-app para facilitarnos la vida. En tu terminal escribe los siguientes comandos:
npx create-react-app user-admin
cd user-admin
yarn add bulma
Hemos creado la carpeta user-admin y dentro de ella agregamos bulma como framework para CSS.
Proyecto Final: React Hooks
El objetivo es poder crear un sencillo User Admin, sin enfocarnos en autenticación ni conexiones a base de datos, nos enfocaremos solamente en React/Bulma y la funcionalidad será; eliminar usuarios y agregar usuarios (usaremos un Modal).
Además en este proyecto NO USAREMOS CLASS COMPONENTS, solamente usaremos Function Components y usaremos los siguientes Hooks: useState, useContext
Video de Youtube
Este tutorial también está grabado y lo puedes encontrar en mi canal de Youtube
Table Layout: Bulma CSS
Creamos el siguiente componente dentro de src/components/Table/index.js
// src/components/Table/index.js
import React from 'react'
function Table() {
return (
<table className="table is-fullwidth">
<thead>
<tr>
<th>id</th>
<th>Name</th>
<th>Username</th>
<th>Phone</th>
<th>City</th>
<th>Destroy</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Juan Pueblo</td>
<td>juancho</td>
<td>333-444-555</td>
<td>Comayagua</td>
<td>X</td>
</tr>
</tbody>
</table>
)
}
export default Table
Importamos el componente Table en src/App.js
// src/App.js
import React from 'react'
import Table from './components/Table'
import 'bulma/css/bulma.min.css'
function App() {
return (
<div className="container">
<Table />
</div>
)
}
createContext()
Vamos a crear un archivo src/context.js donde declararemos el UserContext, podríamos usar este archivo como el archivo central donde creamos los diferentes Context de nuestras Apps, para este ejemplo únicamente usaremos UserContext, pero podríamos usarlo para ThemeContext o guardar la información del Perfil de los usuarios.
// src/context.js
import { createContext } from 'react'
const UserContext = createContext()
export default UserContext
Ahora crearemos el UserProvider dentro de src/containers/UserProvider.js, noten que hemos creado el folder containers para separar componentes que manejan datos, trataremos de crear componentes que manejen datos dentro del folder container.
// src/containers/UserProvider.js
import React, { useState } from 'react'
import UsersContext from '../context'
const UsersProvider = ({ children }) => {
const userState = {
users: [
{
id: 1,
name: 'Leanne Graham',
username: 'Bret',
email: 'Sincere@april.biz',
address: {
street: 'Kulas Light',
suite: 'Apt. 556',
city: 'Gwenborough',
zipcode: '92998-3874',
geo: {
lat: '-37.3159',
lng: '81.1496',
},
},
phone: '1-770-736-8031 x56442',
website: 'hildegard.org',
company: {
name: 'Romaguera-Crona',
catchPhrase: 'Multi-layered client-server neural-net',
bs: 'harness real-time e-markets',
},
},
{
id: 2,
name: 'Ervin Howell',
username: 'Antonette',
email: 'Shanna@melissa.tv',
address: {
street: 'Victor Plains',
suite: 'Suite 879',
city: 'Wisokyburgh',
zipcode: '90566-7771',
geo: {
lat: '-43.9509',
lng: '-34.4618',
},
},
phone: '010-692-6593 x09125',
website: 'anastasia.net',
company: {
name: 'Deckow-Crist',
catchPhrase: 'Proactive didactic contingency',
bs: 'synergize scalable supply-chains',
},
},
],
}
const [users, setUsers] = useState(userState)
return <UsersContext.Provider value={users}>{children}</UsersContext.Provider>
}
export default UsersProvider
Hemos creado la función UsersProvider, la cual recibe un argumento, que puede ser un componente o un conjunto de componentes, se podría pensar en esta función como en un High Order Component (HOC), recibe uno/varios componentes y les agrega data, en este caso un arreglo users, véase
return <UsersContext.Provider value={users}>{children}</UsersContext.Provider>
En este componente también usamos useState el cual es el Hook que nos permite agregar State a Function Component, no es necesario que sea un Class Component, véase:
const [users, setUsers] = useState(userState)
Ahora retornamos a nuestro src/App.js e importamos el UserProvider
import React from 'react'
import './App.css'
import Table from './components/Table'
import UsersProvider from './containers/UserProvider'
import 'bulma/css/bulma.min.css'
function App() {
return (
<UsersProvider>
<div className="container">
<Table />
</div>
</UsersProvider>
)
}
export default App
Al hacerlo, todos los componentes Hijo que estén dentro del UserProvider tienen acceso al listado de Usuarios que declaramos anteriormente. Esto viene siendo como el Provider de nuestra Store cuando usamos Redux. Recuerden que el componente UserProvider recibe uno/varios componentes Hijo como argumento y devuelve un nuevo componente con el arreglo users como valor.
Recibir listado de Usuarios
Vamos a crear un nuevo componente dentro de src/containers/Table/index.js. Este será uno de los componentes que recibirá los valores del context.
import React, { useContext } from 'react'
import UsersContext from '../../context'
import TableHeader from '../../components/Table/Header'
import TableContent from '../../containers/Table/Content'
function Table() {
const { users } = useContext(UsersContext)
return (
<table className="table is-fullwidth">
<TableHeader />
<tbody>
{users.map(user => (
<TableContent key={user.id} user={user} />
))}
</tbody>
</table>
)
}
export default Table
Primero lo primero, hemos importado el UsersContext que se encuentra en nuestro archivo ./src/context.js, aparte que hemos importado un par de nuevos componentes que aún no tenemos; TableHeader y TableContent.
Nótese que hemos importado un nuevo Hook el useContext y lo usamos para poder obtener de nuestro UsersContext el valor que éste nos provee; users.
Al final retornamos la tabla con su TableHeader y el cuerpo de la tabla el listado de usuarios que se pasan al TableContent, a continuación los respectivos componentes que nos hacen falta:
// <TableHeader /> Es un componente presentacional, no maneja state.
// src/components/Table/Header.js
import React from 'react'
function Header() {
return (
<thead>
<tr>
<th>id</th>
<th>Name</th>
<th>Username</th>
<th>Phone</th>
<th>City</th>
<th>Destroy</th>
</tr>
</thead>
)
}
export default Header
A continuación, el componente TableContent:
// src/containers/Table/Content.js
import React from 'react'
function Content({ user }) {
const { id, name, username, phone, address } = user
return (
<tr>
<td>{id}</td>
<td>{name}</td>
<td>{username}</td>
<td>{phone}</td>
<td>{address.city}</td>
<td>
<button className="button is-danger">X</button>
</td>
</tr>
)
}
export default Content
TableContent recibe datos y luego también se conectará al context que hemos creado, es por eso que lo he declarado dentro de containers.
Eliminar Usuarios
Vamos a agregar la funcionalidad para poder eliminar un usuario. A éstas alturas deberías poder listar una tabla con los 2 usuarios que tenemos, así como el botón X para eliminar usuarios, vamos a ponerlo a funcionar.
Para ello, debemos retornar a nuestro UsersProvider:
// src/containers/UserProvider.js
import React, { useState } from 'react';
import UsersContext from '../context';
const UsersProvider = ({ children }) => {
const deleteUser = id => {
setUsers(prevState => {
const users = prevState.users.filter(user => user.id !== id);
return { ...prevState, users };
});
};
const userState = {
users: [...],
deleteUser
};
const [users, setUsers] = useState(userState);
return (
<UsersContext.Provider value={users}>{children}</UsersContext.Provider>
);
};
export default UsersProvider;
Destacamos 2 nuevas cosas; la función deleteUser, la cual recibe un id como argumento:
const deleteUser = id => {
setUsers(prevState => {
const users = prevState.users.filter(user => user.id !== id)
return { ...prevState, users }
})
}
Hemos usado setUsers de useState para filtrar los usuarios excepto el usuario con el id que recibe como argumento. Una vez filtrado actualizamos el state del arreglo users.
Y lo segundo, agregamos dicha función (deleteUser) dentro del objeto userState, el cual pasa como valor a los hijos del UsersProvider, de esta manera podemos tener acceso a dicha función.
const userState = {
users: [...],
deleteUser
};
Perfecto!, espero todavía estén conmigo ;)… Lo siguiente que haremos será poder conectar nuestro componente src/containers/Table/Content.js para poder tener accedo a la función deleteUser.
// src/containers/Table/Content.js
import React, { useContext } from 'react'
import UsersContext from '../../context'
function Content({ user }) {
const { deleteUser } = useContext(UsersContext)
const { id, name, username, phone, address } = user
function onDeleteClick() {
return deleteUser(id)
}
return (
<tr>
<td>{id}</td>
<td>{name}</td>
<td>{username}</td>
<td>{phone}</td>
<td>{address.city}</td>
<td>
<button className="button is-danger" onClick={onDeleteClick}>
X
</button>
</td>
</tr>
)
}
export default Content
Nótese como hemos importado el Hook useContext y luego extraemos la función deleteUser de nuestro UsersContext
const { deleteUser } = useContext(UsersContext)
Agregamos el evento onClick al botón X y al mismo tiempo declaramos una nueva función para poder invocar la reciente funcíon deleteUser.
...
function onDeleteClick() {
return deleteUser(id)
}
...
<button className="button is-danger" onClick={onDeleteClick}>
X
</button>
Agregar nuevos usuarios
Estamos llegando a la última sección de este tutorial, el objetivo será poder agregar nuevos usuarios llenando un formulario. Para ello usaremos un Modal de Bulma y un formulario.
Creamos el siguiente componente src/containers/AddUser/index.js el cual tendrá su propio state y al final se conectará con nuestro context para agregar el nuevo usuario, similar a deleteUser anterior.
import React, { useState } from 'react'
const initialState = {
id: '',
name: '',
username: '',
phone: '',
city: '',
}
function AddUser() {
const [showModal, setShowModal] = useState(false)
const [values, setValues] = useState(initialState)
function handleModal() {
return setShowModal(!showModal)
}
function onChange({ target: { name, value } }) {
return setValues({
...values,
[name]: value,
})
}
const { name, username, phone, city } = values
return (
<>
<button className="button is-primary" onClick={handleModal}>
Add user
</button>
<div className={`modal ${showModal ? 'is-active' : ''}`}>
<div className="modal-background" />
<div className="modal-card">
<header className="modal-card-head">
<p className="modal-card-title">Add user</p>
<button
className="delete"
aria-label="close"
onClick={handleModal}
/>
</header>
<section className="modal-card-body">
<form>
<div className="field">
<label className="label">Name</label>
<div className="control">
<input
className="input"
type="text"
name="name"
value={name}
onChange={onChange}
/>
</div>
</div>
<div className="field">
<label className="label">Username</label>
<div className="control">
<input
className="input"
type="text"
name="username"
value={username}
onChange={onChange}
/>
</div>
</div>
<div className="field">
<label className="label">Phone</label>
<div className="control">
<input
className="input"
type="text"
name="phone"
value={phone}
onChange={onChange}
/>
</div>
</div>
<div className="field">
<label className="label">City</label>
<div className="control">
<input
className="input"
type="text"
name="city"
value={city}
onChange={onChange}
/>
</div>
</div>
<button
className="button is-primary"
type="submit"
onClick={handleModal}
>
Submit
</button>
</form>
</section>
</div>
</div>
</>
)
}
export default AddUser
Bien, recorramos el código, hemos importando el Hook useState para controlar el mostrar/ocultar del Modal
const [showModal, setShowModal] = useState(false)
Tambíen usamos useState para crear el State del formulario, será un Formulario Controlado
const [values, setValues] = useState(initialState)
Las funciones handleModal y onChange:
...
// El objetivo es poder mostrar/ocultar el modal
function handleModal() {
return setShowModal(!showModal)
}
// El objetivo es poder controlar el state de cada Input
function onChange({ target: { name, value } }) {
return setValues({
...values,
[name]: value,
})
}
...
// Nos permite poder Abrir el Modal
<button className="button is-primary" onClick={handleModal}>
Add user
</button>
...
En Bulma, para poder abrir un Modal, ocupamos activarle una clase conocida como is-active. Lo hacemos de la siguiente forma:
// Si el State showModal es true, se agrega la clase, de lo contrario no
// De esta forma controlamos el mostrar el Modal.
// Nótese que usamos backticks y un if ternario
<div className={`modal ${showModal ? 'is-active' : ''}`}>
Por último, agregamos una serie de Inputs con las clases de Bulma para Formularios y les asignamos las propiedades value y onChange. Las propiedades value provienen del State local que definimos para los Inputs.
...
// State local
const { name, username, phone, city } = values
...
<div className="field">
<label className="label">Name</label>
<div className="control">
<input
className="input"
type="text"
name="name"
value={name}
onChange={onChange}
/>
</div>
</div>
Una vez finalizado nuestro componente AddUser, lo importamos en src/App.js
import React from 'react'
import './App.css'
import Table from './containers/Table'
import UsersProvider from './containers/UserProvider'
import AddUser from './containers/AddUser'
import 'bulma/css/bulma.min.css'
function App() {
return (
<UsersProvider>
<div className="container">
<Table />
<AddUser />
</div>
</UsersProvider>
)
}
export default App
Ahora viene la función addUser dentro de nuestro UsersProvider src/containers/UserProvider.js, proceso similar al de deleteUser:
...
const addUser = user => {
setUsers(prevState => ({
...prevState,
users: [user, ...prevState.users],
}))
}
const userState = {
users: [...],
deleteUser,
addUser
};
...
Listo!. Hemos agregado la función de addUser en el Provider, pero ahora debemos usarla dentro de AddUser, así que en el componente src/containers/AddUser/index.js
...
import uuid from 'uuid';
import UsersContext from '../../context';
...
function AddUser() {
...
const { addUser } = useContext(UsersContext);
...
function onSubmit(e) {
e.preventDefault();
const newUser = {
id: uuid(),
name: values.name,
username: values.username,
phone: values.phone,
address: { city: values.city }
};
addUser(newUser);
setValues(initialState);
}
...
<form onSubmit={onSubmit}>
...
Hemos conectado AddUser con nuestro Context, extraemos la función addUser y además creamos una nueva función para el Form; onSubmit. La llamamos onSubmit. Dicha función previene el comportamiento por default del navegador con los Formularios y creamos un nuevo objeto el cual enviaremos como argumento para addUser(newUser).
Nótese que para el id, hemos usado la librería uuid(), la cual debemos instalar como dependencia de nuestro proyecto e importarla. En una nueva terminal escribe el siguiente comando:
yarn add uuid
Aplicando el DRY
Debemos aplicar el Dont Repeat Yourself, como regla en nuestros proyectos, por lo tanto vamos a crear un componente InputForm dentro de src/components/InputForm/index.js, el cual es solamente presentacional, no posee propio state.
import React from 'react'
export default function InputForm({ label, type, name, value, onChange }) {
return (
<div className="field">
<label className="label">{label}</label>
<div className="control">
<input
className="input"
type={type}
name={name}
value={value}
onChange={onChange}
/>
</div>
</div>
)
}
Recibe una serie de props, los cuales usamos para el Input. Ahora debemos actualizar src/containers/AddUser/index.js
...
import InputForm from '../../components/InputForm';
...
<form onSubmit={onSubmit}>
<InputForm
label="Name"
type="text"
name="name"
value={name}
onChange={onChange}
/>
<InputForm
label="Username"
type="text"
name="username"
value={username}
onChange={onChange}
/>
<InputForm
label="Phone"
type="text"
name="phone"
value={phone}
onChange={onChange}
/>
<InputForm
label="City"
type="text"
name="city"
value={city}
onChange={onChange}
/>
...
</form>
...
Código Fuente
Conclusión
useContext y CONTEXT API pueden ser una opción para no tener que vernos forzados en usar Redux, se recomienda sobre todo cuando tenemos cambios de Themes en nuestros proyectos, tambien para actualizar la data de un usuario que inicia sesión en la aplicación. Espero les haya servido!
I write both English & Spanish about different subjects. You can follow me on Twitter