Custom Hook: useLocalStorage

Custom Hook: useLocalStorage

Vamos a construir nuestro propio hook que nos ayude a trabajar con el localStorage en nuestras aplicaciones.

En este artículo vamos a centrarnos en la construcción de un custom hook que utilizaremos para poder trabajar con el localStorage en nuestras aplicaciones de React, hook al que vamos a llamar `useLocalStorage' y para ello vamos a partir de una aplicación muy simple en la que se nos estará mostrando la siguiente interfaz de usuario:

La interfaz que nos presenta la aplicación es realmente simple puesto que únicamente tenemos a nuestra disposición un campo de texto en el que podremos escribir lo que queramos y tres botones Set, Get y Remove aunque en este punto cada uno de estos tres botones no va a hacer nada puesto que no tienen ninguna función asociado que esté presente en el código. Nuestra misión, por lo tanto va a ser crear el hook useLocalStorage el cual va a proporcionarnos tres funciones: una para poder establecer un valor dentro del localStorage, otra para poder obtener un valor del localStorage y una última opción que nos permitirá borrar un elemento del localStorage.

Así pues el código de partido del componente que se estará encargando de mostrar el contenido que se renderiza en la imagen anterior será algo parecido a lo que se puede ver a continuación:

import { useState } from 'react'

export const Page = () => {
  const [value, setValue] = useState('')

  return (
    <div>
      <h1 className='mb-2 text-3xl font-bold'>useLocalStorage</h1>
      <input
        className='mb-4'
        onChange={e => setValue(e.target.value)}
        type='text'
        value={value}
      >
      <div className='flex flex-row gap-4'>
        <button>Set</button>
        <button>Get</button>
        <button>Remove</button>
      </div>
    </div>
  )
}

Como podemos observar el código de nuestro componente es sencillo puesto que dentro del mismo estamos definiendo un valor de su estado al que llamamos value y este será el que asignaremos como valor que tendrá el campo <input> que se renderiza para poder interactuar con el usuario.

Por lo tanto lo que ahora vamos a tener que hacer es crear nuestro custom hook junto con cada una de las tres funciones que vamos a poder asociar a cada uno de los botones que se muestran en la interfaz y que servirán para interactuar con los elementos que tenemos dentro del localStorage.

Creación de useLocalStorate

Lo primero que vamos a hacer es crear el fichero useLocalStorage.ts para guardar dentor del mismo el código de nuestro custom hook y lo que haremos dentro del mismo será definir la función que representará a nuestro custom hook como sigue:

export const useLocalStorate = () => {}

Una vez que hemos construido nuestro hook lo que vamos a hacer es pasar a utilizarlo dentro de nuestro componente Page y para ello, como sucede con cualquier otro hook, lo primero que haremos será importarlo:

import { useLocalStorage } from './useLocalStorate'

y ahora lo que pasaremos a hacer es invocarlo en nuestro componente:

export const Page = () => {
  const [value, setValue] = useState('')
  useLocalStorage()
  // ... resto del código

Nota: el hecho de crear un custom hook en un fichero independiente y posteriormente invocarlo en el componente donde se utiliza es la forma que se considera correcta de proceder cuando estamos trabajando en React.

Establecer un valor en el localStorage

Vamos a crear la primera de las funciones que nos tiene que proporcionar useLocalStorage que no es otra que aquella que nos permita guardar un valor dentro del localStorage del navegador donde se está ejecutando nustra aplicación. Ahora bien, en este punto es necesario saber que en el localStorage la información que se guardará siempre estará basada en una key y esto es así puesto que el navegador nos permitirá trabajar con el localStorage a traves de una llamada del tipo:

window.localStorage.setItem(key, value)

Nota: podemos pensar en el localStorage del navegador como en una espacie de Map.

¿Cuál es el problema que nos encontramos ahora? Pues que en nuestra definición de useLocalStorage no hemos pensado en ningún momento en el uso de esta key por lo que nos vamos a ir a la definición de nuestro hook de tal manera que cuando este sea invocado va a esperar recibir esta key como un parámetro, lo que nos deja algo como lo siguiente:

export const useLocalStorage = (key: string) => {}

Esto automáticamente lo que provoca es que tengamos que especificar esta key dentro de la llamada que se hace del hook en el componente Page por lo que escribiremos algo como lo siguiente donde optamos por llamar a la key value pero podría ser lo que nosotros quisiésemos:

export const Page = () => {
  const [value, setValue] = useState('')
  useLocalStorage('value')
  // ... resto del código

Como ahora mismo ya tenemos la key con la que trabajaremos dentro de nuestro custom hook vamos a centrarnos en al construcción de la función que nos permita establecer el valor en el localStorage. La definición de esta función nos deja algo como lo siguiente:

export const useLocalStorage = (key: string) => {
  const setItem = (value: unknown) => {
    window.localStorage.setItem(key, JSON.stringify(value))
  }
}

Lo primero que que tenemos que entender de esta definición es que el parámetro value que recibe setItem es de tipo unknown puesto que desconocemos cuál es el tipo que se va a guardar en el localStorage y además no vamos a tener que preocuparnos por ello. Además estamos haciendo uso del método setItem que nos proporciona el objeto localStorage de window (objeto que se encarga de todas las interacción con el localStorage del navegador) y utilizamos en el método stringify del objeto JSON para que en el caso de que el valor a guardar que sea un objeto de JavaScript o un array este método lo convierta previamente a string antes de que los guardemaos en el localstorage.

Nos queda un detalle más en el código que estamos desarrollando y es que useLocalStorage tienen que devolver esta función para que pueda ser utilizada en todos los componentes que vayan a usar nuestro custom hook por lo que escribiremos:

export const useLocalStorage = (key: string) => {
  const setItem = (value: unknown) => {
    window.localStorage.setItem(key, JSON.stringify(value))
  }

  return {
    setItem
  }
}

La pregunta que puede surgir en este punto es ¿por qué estamos retonando un objeto y no la función setItem directamente? La respuesta es sencilla puesto que nuestra intención es que useLocalStorage retorne más de una función y la forma correcta de hacerlo es como métodos de un objeto.

Si nos vamos a ahora al componente Page vamos a poder capturar esta función:

export const Page = () => {
  const [value, setValue] = useState('')
  const { setItem } = useLocalStorage('value')
  // ... resto del código

Y ahora tan soló nos quedará vincular la llamada a esta función en el momento en el que pulsa sobre el botón que está etiquetado como Set en el código JSX del componente:

<button onClick={() => setItem(value)}>Set</button>

donde lo que estamos haciendo es que cada vez que se pulse sobre el botón Set llamaremos a la función setItem que nos proporciona useLocalStorage pasándole como valor que queremos establecer en el localStorage lo que tenga el atributo del estado value que recordemos que es lo que esté escrito en el <input> que se está renderizando en la interfaz.

Si ahora volvemos a nuestra aplicación en el navegador, tecleamos something en el cuadro de texto, pulsamos sobre Set y nos vamos a pestaña Aplication dentro de la herramientas para desarrolladores, desplegando la opción Local Storage y seleccionando nuestra aplicación podremos ver que se ha creado la clave value y que el valor que tiene asociado es something tal y como esperábamos:

Con esto estaremos validando que la función setItem que hemos construido en nuestro custom hook está funcionando y que esta nos permite guardar un elemento dentro del localStorage de nuestro navegador.

Pero antes de dar por finaliza esta implementación tenemos que tener cuidado puesto que no todos los navegadores con los que los usuarios acceden a nuestra aplicación van a tener implementado un localStorage y, en esos casos, nuestro código acabaría dando un error puesto que se trataría de una situación que el método setItem del objeto localStorage de window interpreta como que el navegador no tiene más espacio (en realidad no tiene ningún espacio) para guardar un elemento y por lo tanto lanzará un error.

Así pues podemos encerrar el código que está dentro del método setItem dentro de un bloque try-catch de tal manera que si se produce un error seamos capaces de mostrarle un mensaje al usuario de que ha ocurrido un problema. Con el objetivo de hacer las cosas lo más sencillas posible en nuestra implementación lo que vamos a hacer es que en el caso de que se produzca un error simplemente lo escribiremos por la consola:

export const useLocalStorage = (key: string) => {
  const setItem = (value: unknown) => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value))
    } catch (error) {
      console.log(error)
    }
  }

  return {
    setItem
  }
}

Obtener un valor del localStorage

Vamos con la segunda de las funcionalidades que nos tiene que proporcionar useLocalStorage que no es otra que el ofrecernos un método que nos permita obtener un valor que haya sido almacenado en el localStorage. Así pues definimos la siguiente función dentro de useLocalStorage:

export const useLocalStorage = (key: string) => {
  const getItem = () => {
    try {
      const item = windown.localStorage.getItem(key)
      return item ? JSON.parse(item) : undefined
    } catch (error) {
      console.log(error)
    }
  }
  // Resto del código

  return {
    getItem,
    setItem
  }
}

Como podemos ver el código de nuestra función getItem queda envuelto en una instrucción try-catch por los mismos motivos que hemos explicado para el caso de la funicón setItem y dentro de ella lo que hacemos es intentar recuperar el item que se ha guardado bajo la clave que está asociada nuestro hook dentro del localStorage de tal manera que si existe lo que hacemos es retorna ese valor tras parsearlo como un JSON gracias al método parse del objeto JSON de JavaScript.

Nota: el método parse del objeto JSON lo que va a hacer es convertir los posibles arrays que hayamos guardado con el método stringify en el método setItem a un array de JavaScript o en el caso de haber guardado un objeto a un objeto de JavaScript. Si se hubiese guardado un tipo primitivo este método lo que hace es devolver el tipo primitivo.

Además vemos como en el objeto que retornará useLocalStorage además se estará devolviendo el método getItem de tal manera que pueda ser utilizado en cualquier sitio donde se esté haciendo uso del hook.

Sabiendo esto podemos volver al código de nuestro componente Page y obtener esta función:

export const Page = () => {
  const [value, setValue] = useState('')
  const { getItem, setItem } = useLocalStorage('value')
  // ... resto del código

y vincularla con el botón Get dentro del código JSX de tal manera que cada vez que este sea pulsado se obtenga ese valor y, nuevamente por mantener las cosas sencillas, lo que haremos será mostrar ese valor en la consola del navegador por lo que escribiremos:

<button
  onClick={() => {
    console.log(getItem())
  }}
>
  Get
</button>

Si ahora volvemos a nuestra aplicación en el navegador y escribimos something else en el cuadro de texto, pulsamos sobre Set y posteriormente pulsamos sobre Get podemos observar como en la consola nos aparecerá el mensaje something else tal y como esperamos:

Eliminar un valor del localStorage

Ya solamente nos queda una función más por implementar dentro de nuestro custom hook y no es otra que aquella que nos permita eliminar un elemento de dentro del localStorage. Así pues nos dirigimos una vez más al código de useLocalStorage y definimos lo siguiente:

export const useLocalStorage = (key: string) => {
  const removeItem = () => {
    try {
      window.localStorage.removeItem(key)
    } catch (error) {
      console.log(error)
    }
  }

  // Resto del código.

  return {
    getItem,
    removeItem,
    setItem
  }
}

Con lo que hemos estado explicando hasta ahora el código anterior parece bastante claro pero por si acaso decir que estamos envolviendo todo el contenido de la función en un bloque try-catch para evitar que la aplicación falle y dentro del mismo lo que hacemos es llamar al método removeItem que nos proporciona el objeto localStorage de window que es el encargado de eliminar del localStorage del navegador el valor que esté asociado a la clave que se recibe como parámetro que es precisamente lo que nosotros queremos.

Además estamos añadiendo la función removeItem como un método más del objeto que retorna useLocalStorage de tal manera que pueda ser usado en el lugar en el que se invoque a nuestro hook cosa que vamos a hacer en componente Page:

export const Page = () => {
  const [value, setValue] = useState('')
  const { getItem, removeItem, setItem } = useLocalStorage('value')
  // ... resto del código

Y ahora lo que hacemos es vincularlo como al evento onClick dentro del botón etiquetado como Remove dentro del código JSX del componente:

<button onClick={() => removeItem()}>Remove</button>

Si ahora volvemos a nuestra aplicación si pulsamos sobre el botón Remove y posteriormente sobre el botón Get veremos que en la consola se nos muestra el valor undefined tal y como esperaríamos.

Resumen

Al final lo que hemos conseguido es tener la funcionalidad que nos permitirá interactuar con el localStorage en un único punto dentro de nuestra aplicación (en el custom hook) useLocalStorage de tal manera que dicha funcionalidad va a ser fácilmente reutilizable y mantenible lo cual siempre es una buena práctica de programación.

Código completo

El código completo del custom hook useLocalStorage es el que se recoge a continuación:

export const useLocalStorage = (key: string) => {
  const getItem = () => {
    try {
      const item = windown.localStorage.getItem(key)
      return item ? JSON.parse(item) : undefined
    } catch (error) {
      console.log(error)
    }
  }

  const setItem = (value: unknown) => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value))
    } catch (error) {
      console.log(error)
    }
  }

  const removeItem = () => {
    try {
      window.localStorage.removeItem(key)
    } catch (error) {
      console.log(error)
    }
  }

  return {
    getItem,
    removeItem,
    setItem
  }
}

Y el código completo del componente que nos ha servido de ejemplo durante la explicación que se ha seguido en el artículo es el siguiente:

import { useState } from 'react'
import { useLocalStorage } from './useLocalStorate'

export const Page = () => {
  const [value, setValue] = useState('')
  const { getItem, removeItem, setItem } = useLocalStorage('value')

  return (
    <div>
      <h1 className='mb-2 text-3xl font-bold'>useLocalStorage</h1>
      <input
        className='mb-4'
        onChange={e => setValue(e.target.value)}
        type='text'
        value={value}
      >
      <div className='flex flex-row gap-4'>
        <button onClick={() => setItem(value)}>Set</button>
        <button
          onClick={() => {
            console.log(getItem())
          }}
        >
          Get
        </button>
        <button onClick={() => removeItem()}>Remove</button>
      </div>
    </div>
  )
}