Refuerzo React Formar – 2021
Hooks y custom hooks
Contenido
Profundizar en el tema de los Hooks
-
Crear otros customHooks
-
useState
-
useEffect
-
useCount(custom hook)
-
useForm (custom hook)
Los ejemplos aquí propuestos brindan un panorama simple y sencillo en la implementación de los hooks que provee React. Además se propone la creación de custom hooks que serán reutilizados en ejercicios más complejos
Recursos
- Bootstrap
Documentación
- useState
- useEffect
Guía práctica
CounterApp
- Creo el siguiente componente:
import React, {useState} from 'react';
import './counter.css'
export const CounterApp = () => {
const [state, setState] = useState({
counter1 : 10,
counter2 : 20
});
const {counter1, counter2} = state
return (
<>
<h1>Counter {counter1}</h1>
<h1>Counter {counter2}</h1>
<hr/>
<button onClick={() => setState({
...state,
counter1 : counter1 + 1,
})} className="btn btn-primary">+1</button>
</>
)
}
- Inicializa el estado con un objeto que puede tener varias propiedades
- El ejemplo está orientado en setear el estado, manteniendo los valores del objeto con spreed operator y modificar solo una de sus propiedades cada vez que se haga click en el botón
CounterWithCustomHook
- Creo el siguiente componente:
import React from 'react';
import useCounter from '../../hooks/useCounter';
import './counter.css';
function CounterWithCustomHook() {
const {state:counter, incrementar, decrementar, reset} = useCounter(100)
return (
<>
<h1>Counter with Hook : {counter}</h1>
<hr/>
<button onClick={() => decrementar(2)} className="btn btn-primary mx-2">-1</button>
<button onClick={reset} className="btn btn-danger">reset</button>
<button onClick={() => incrementar(2)} className="btn btn-primary mx-2">+1</button>
</>
)
}
export default CounterWithCustomHook
- Importo un hook personalizado o custom hook llamado useCount del cual, desestructuracion por medio, extraigo el state (estado) renombrandolo counter, y las funciones incrementar, decrementar y reset.
- Este componente muestra el contador, que toma el valor que devuelve el state del useCount el cual es modificado por el evento click de los botones.
- Las funciones incrementar y decrementar pueden recibir un parámetro que indica el factor, es decir cada cuánto se debe incrementar o decrementar, siendo 1 el valor por defecto.
- La función reset no recibe ningún parámetro, así que solo envía el event por ello no necesita ser invocada dentro de un callback.
- Creo un custom hook llamado useCounter
import { useState } from "react";
function useCounter(initialState = 10) {
const [state, setState] = useState(initialState)
const incrementar = (factor = 1) => setState(state + factor); //por defecto el factor tiene valor a 1
const decrementar = (factor = 1) => setState(state -factor); //por defecto el factor tiene valor a 1
const reset = () => setState(initialState)
return {
state,
incrementar,
decrementar,
reset
}
}
export default useCounter
- Puede recibir un estado inicial, de lo contrario se inicializa en 10
- Tiene tres métodos que modifican el estado
- Retorna el estado, y los métodos
FormSimple | useEffect
- Creo un componente llamado FormSimple
import React, {useEffect, useState} from 'react';
import './styles.css'
function FormSimple() {
const [formState, setFormState] = useState({
name : '',
email : ''
});
const {name, email} = formState;
useEffect( () => {
console.log('Hey! useEffect')
}, [])
useEffect( () => {
console.log('formState cambió!')
}, [formState])
useEffect( () => {
console.log('email cambió!')
}, [email])
const handleInputChange = ({target}) => {
/* selecciono el input cuyo atributo name tenga valor'name' con [target.name] y seteo su valor con target.value. Lo mismo hago con email*/
setFormState({
...formState,
[target.name] : target.value,
})
console.log(formState)
}
return (
<>
<h4>useEffect</h4>
<hr/>
<div className="form-group my-2">
<input
type="text"
name="name"
className="form-control"
placeholder="tu nombre"
autoComplete="off"
value = {name}
onChange={handleInputChange}
/>
</div>
<div className="form-group my-2">
<input
type="text"
name="email"
className="form-control"
placeholder="tu email"
autoComplete="off"
value = {email}
onChange={handleInputChange}
/>
</div>
</>
)
}
export default FormSimple
- El estado inicial es un objeto con dos propiedades:
name
yemail
, ambas con valor de un string vacío - Desestructuro el state
const {name, email} = formState;
para un mejor manejo de las variables. - Pruebo aplicar un useEffect que mostrará un Coords por consola una sola vez, siempre y cuando se pase como segundo argumento un array vacío
useEffect( () => {
console.log('Hey! useEffect')
}, [])
- Puedo ejecutar alguna funcionalidad si cambia algún elemento en particular. Para ello lo paso como un elemento del array que va como segundo parámetro del useEffect
useEffect( () => {
console.log('formState cambió!')
}, [formState])
useEffect( () => {
console.log('email cambió!')
}, [email])
- Creo el método
handleInputChange
para manejar los cambios producidos en los inputs
const handleInputChange = ({target}) => {
/* selecciono el input cuyo atributo name tenga valor'name' con [target.name] y seteo su valor con target.value. Lo mismo hago con email*/
setFormState({
...formState,
[target.name] : target.value,
})
}
- Desestructuro del e el target
- Seteo el FormState pasandole con spreed operator el estado actual y luego, haciendo uso de la propiedad
name
yvalue
, seteo los valores del input modificado - En cada input aplico el evento onChange, donde se llama al método handleInputChange, el cual como no recibe ningún parámetro, se llama directamente enviando implícitamente el primer argumento, es decir el event
Coords | cleanUp
- Creo un componente llamado Coords
import React, { useEffect, useState } from 'react';
function Coords() {
const [coords, setCoords] = useState({x:0, y:0});
const {y,x} = coords
useEffect(() => {
console.log('componente montado!');
const mouseMove = e => {
const coors = {x:e.x, y:e.y};
//console.log(coors);
setCoords(coords)
//console.log(':D')
}
window.addEventListener('mousemove', mouseMove)
return () => {
console.log('componente desmontado!')
window.removeEventListener('mousemove', mouseMove)
};
}, []);
return (
<div>
<h4>Coordenadas</h4>
<p>
x: {x},
y: {y}
</p>
</div>
)
}
export default Coords
- Implemento useEffect y como quiero que se ejecute una sola vez, le paso un array vacío como segundo parámetro.
- De manera que cuando se monta el componente, se mostrará por consola ‘componente montado’, y se ejecuta el evento de escucha mousemove, que llamará a la función
mouseMove
, la cual muestra por consola un Coords - El useEffect puede retornar una callback que ejecutará la remoción de evento de escucha, a los efetos de liberar la memoria, de contrario los eventos de escucha se irían incrementarando exponencialmente
FormWithHook
import React, { useState} from 'react';
import useForm from '../../hooks/useForm';
import './styles.css';
function FormWithHook() {
const [formState, handleInputChange, reset] = useForm({
name : '',
email : '',
password : '',
});
const {name, email, password} = formValues;
const handleSubmit = (e) => {
e.preventDefault();
console.log(formValues);
reset()
}
return (
<form onSubmit={handleSubmit}>
<h4>FormWithHook</h4>
<hr/>
<div className="form-group my-2">
<input
type="text"
name="name"
className="form-control"
placeholder="tu nombre"
autoComplete="off"
value = {name}
onChange={handleInputChange}
/>
</div>
<div className="form-group my-2">
<input
type="text"
name="email"
className="form-control"
placeholder="tu email"
autoComplete="off"
value = {email}
onChange={handleInputChange}
/>
</div>
<div className="form-group my-2">
<input
type="password"
name="password"
className="form-control"
placeholder="*****"
value = {password}
onChange={handleInputChange}
/>
</div>
<button className="btn btn-primary">Guardar</button>
</form>
)
}
export default FormWithHook
- Extraigo el manejador de input handleInputChange y creo un nuevo hook llamado useForm
import React, { useState } from 'react'
function useForm(initialState = {}) {
const [state, setState] = useState(initialState);
const handleInputChange = ({ target }) => {
setState({
...state,
[target.name]: target.value,
})
}
return [state, handleInputChange]
}
export default useForm
- De manera que desde mi FormWithHook importo el hook useForm y aplicandole destructuración me traigo el estado y el manejador de inputs:
const [formState, handleInputChange] = useForm({
name : '',
email : '',
password : '',
});
- El evento onChange de los inputs se sigue manejando de la misma manera, solo que en el useForm se encuentra la lógica:
<input
type="text"
name="name"
className="form-control"
placeholder="tu nombre"
autoComplete="off"
value = {name}
onChange={handleInputChange}
/>
useFetch | custom hooks
- Creamos un hooks que permite hacer pedidos a un api a partir de una url que recibe por parámetro.
- Utilizando el hook useRef creo una referencia cuyo valor inicial es true.
- Cuando el componente donde se utiliza este custom hook se desmonta se ejecuta el return de un useEffect cambiando el valor de referencia a false.
- Creamos el estado inicial con useState, que consite en un objeto literal que contiene loading, error y data.
- Utilizamos un useEffect que se ejecutará sólo una vez.
- Este primero setea el estado a su valor incial
- Luego hace un pedido a la API utilizando la URl que se recibe por parámetro.
- Por último chequea si el componente está cargado, es decir cheque que la referencia tenga valor true para setear el estado con la información que proviene de la API. De esta manera se evita setear el estado de un componente que haya sido desmontado.
import { useState, useEffect, useRef } from 'react'
function useFetch(url) {
const isMount = useRef(true);
useEffect(() => {
return () => {
isMount.current = false
}
}, [])
const [state, setState] = useState({
loading: true,
error: null,
data: null
})
useEffect(() => {
setState({
loading: true,
error: null,
data: null
})
fetch(url)
.then(response => response.json())
.then(data => {
if (isMount.current) {
setState({
loading: false,
error: null,
data
})
}
})
}, [url]);
return state
}
export default useFetch
Quote
- Creamos el siguiente Quote
- Importamos los custom hooks useFetch y useCount
- El useCount se inicializa en 1
- Se obtiene el loading y la data del useFetch (por defecto false y null respectivamente)
- Cuando data sea true, se obtiene el author y la data
- Se muestra la información obtenida en pantalla
- Haciendo click en el boton se setea el estado de useCount con su método incrementar, cambiando la URL y por lo tanto haciendo otro pedido a la API
import useCount from '../hooks/useCount';
import useFetch from '../hooks/useFetch';
import "./styles.css";
function Quote() {
const {state : count, incrementar} = useCount(1);
const {loading, data} = useFetch(`https://www.breakingbadapi.com/api/quotes/${count}`);
const {author, quote} = !!data && data[0];
return (
<div>
<h4>Quote</h4>
<hr/>
{
loading
?
(
<div className="alert alert-info text-center">
Loading
</div>
)
:
(
<blockquote className="blockquote text-end">
<p className="mb-2">{quote}</p>
<footer className="blockquote-footer">
{author}
</footer>
</blockquote>
)
}
<button
className="btn btn-outline-dark"
onClick = { () => incrementar()}
>
Siguiente quote
</button>
</div>
)
}
export default Quote
useRef | Example
- Creamos un componente llamado RefExample
- Utilizamos el componente Quote. El mismo se mostrará u ocultará al hacer click en el botón.
- Con el useRef y el cleanup de useEffect aplicado al useFetch se evita que se setee el estado si el componente está oculto.
import {useState} from 'react';
import Quote from './Quote';
import "./styles.css";
function RefExample() {
const [show, setShow] = useState(false);
return (
<div>
<h4>RefExample</h4>
<hr/>
{show && <Quote/>}
<button
className="btn btn-outline-dark mt-4"
onClick={ () => setShow(!show)}
>
Show/hidden
</button>
</div>
)
}
export default RefExample
Memoriza | memo de React
- Creamos un componente padre que tendrá la capacidad de setear su estado al hacer click en el botón.
- Este componente tiene un componente hijo que se renderizará cada vez que cambie el estado de su componente padre. Esto se debe evitar.
import {useState} from 'react';
import useCount from '../hooks/useCount';
import {Small} from './Small';
function Memorize() {
const {state : count, incrementar} = useCount(10);
const [show, setShow] = useState(true);
return (
<div>
<h4>Memorize</h4>
<hr/>
<Small count={count}/>
<button
className="btn btn-outline-dark mt-3"
onClick={()=> incrementar()}
>
+1
</button>
<button
className="btn btn-outline-dark mt-3 mx-2"
onClick={ () => setShow(!show)}
>
Show/hidden {JSON.stringify(show)}
</button>
</div>
)
}
export default Memorize
- A fin de evitar que el componente hijo se vuelva a renderizar sin tener justifiación se implementa memo de React en dicho componente:
import React from 'react'
export const Small = React.memo(({count}) => {
console.log('Me renderizaron!!')
return (
<h5>Count: <small> {count}</small></h5>
)
})
useMemo | MemoHook
- Creamos el componente que implementa el uso de una función que conlleva un proceso pesado.
- Este proceso de ejecutará cada vez que cambie el estado del componente.
- A fin de evitar que esto ocurra de manera injustificada se implementa el uso de un hook llamado useMemo
- Este recibe un callback y un input, es decir está atento a qué estado debe estar pendiente para ejecutar
import {useMemo} from 'react';
import {useState} from 'react';
import useCount from '../hooks/useCount';
export const MemoHook = () => {
const {state : count, incrementar} = useCount(5000);
const [show, setShow] = useState(true);
const procesoPesado = (iteraciones) => {
for (let i = 0; i < iteraciones; i++) {
//console.log('iterando...')
}
return iteraciones + ' iteraciones realizadas';
}
const memoProcesoPesado = useMemo(() => procesoPesado(count), [count])
return (
<div>
<h4>MemoHook</h4>
<hr/>
<h5>Count: <small> {count}</small></h5>
<p>{memoProcesoPesado}</p>
<button
className="btn btn-outline-dark mt-3"
onClick={()=> incrementar()}
>
+1
</button>
<button
className="btn btn-outline-dark mt-3 mx-2"
onClick={ () => setShow(!show)}
>
Show/hidden {JSON.stringify(show)}
</button>
</div>
)
}