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 y email, 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 y value, 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>
    )
}

GitHub

View Github