Administrando el estado con React Query. 〽️

franklin030601

Franklin Martinez

Posted on March 17, 2023

Administrando el estado con React Query. 〽️

React Query es una librería grande y completa que facilita el trabajo a la hora de realizar peticiones del lado del cliente hacia el servidor e incluso realiza mucho mas que eso.

Pero ¿Sabias que puedes usar esta librería como administrador de estado?, posiblemente una alternativa a redux-toolkit, zustand, entre otros. En este articulo te mostrare como implementarlo de esta manera.

🚨 Nota: para entender este articulo debes de tener conocimiento básico de como se usa React Query y también algo de conocimiento básico con TypeScript.

 

Tabla de contenido.

📌 Tecnologías a utilizar.

📌 Creando el proyecto.

📌 Primeros pasos.

📌 Creando las paginas.

📌 Configurando React Query.

📌 Usando React Query como administrador de estado.

📌 Creando las funciones para hacer las peticiones.

📌 Obteniendo los datos con React Query.

📌 Agregando nueva data a nuestro estado.

📌 Eliminando los datos del estado.

📌 Actualizando los datos del estado.

📌 Conclusión.

📌 Demostración.

📌 Código fuente.

 

📢 Tecnologías a utilizar.

  • React JS 18.2.0
  • React Query 4.20.4
  • React Router Dom 6.6.1
  • TypeScript 4.9.3
  • Vite JS 4.0.0
  • CSS vanilla (Los estilos los encuentras en el repositorio al final de este post)

📢 Creando el proyecto.

Al proyecto le colocaremos el nombre de: state-management-rq (opcional, tu le puedes poner el nombre que gustes).

npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

Creamos el proyecto con Vite JS y seleccionamos React con TypeScript.

Luego ejecutamos el siguiente comando para navegar al directorio que se acaba de crear.

cd state-management-rq
Enter fullscreen mode Exit fullscreen mode

Luego instalamos las dependencias.

npm install
Enter fullscreen mode Exit fullscreen mode

Después abrimos el proyecto en un editor de código (en mi caso VS code).

code .
Enter fullscreen mode Exit fullscreen mode

📢 Primeros pasos.

Primero vamos a instalar react router dom para poder crear un par de paginas en nuestra app.

npm install react-router-dom
Enter fullscreen mode Exit fullscreen mode

Entonces, vamos a crear una carpeta src/layout para crear un menu de navegación muy sencillo que estará en todas las paginas.
Dentro de src/layout creamos el archivo index.tsx y agregamos lo siguiente:

import { NavLink, Outlet } from 'react-router-dom'

type LinkActive = { isActive: boolean }

const isActiveLink = ({ isActive }: LinkActive) => `link ${isActive ? 'active' : ''}`

export const Layout = () => {
    return (
        <>
            <nav>
                <NavLink className={isActiveLink} to="/">Home 🏠</NavLink>
                <NavLink className={isActiveLink} to="/create">Create ✍️</NavLink>
            </nav>

            <hr className='divider' />

            <div className='container'>
                <Outlet />
            </div>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

Luego en el archivo src/App.tsx vamos a borrar todo. Y vamos a crear nuestras rutas básicas.

Nota: Vamos a establecer las rutas mediante mediante createBrowserRouter, pero si deseas puedes usar los componentes que aun dispone react-router-dom como <BrowserRouter/>, <Routes/>, <Route/>, etc. en vez de createBrowserRouter

Mediante createBrowserRouter vamos crear un objeto donde iremos agregando nuestras rutas. Nota que solo tengo un ruta padre, y lo que muestro es el menu de navegación, y esta ruta tiene 3 rutas hijas, que por el momento no se han creado sus paginas.

Finalmente creamos el componente App que exportamos por defecto, este componente va a renderizar un componente de react-router-dom que es el <RouterProvider/> que recibe el router que acabamos de crear.

Y con esto ya podemos navegar entre las diferentes rutas.

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { Layout } from './layout';
import { Home } from './pages/home';

const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      {
        index: true,
        element: <>create</>,
      },
      {
        path: "/create",
        element: <>create</>,
      },
      {
        path: "/:id",
        element: <>edit</>,
      },
    ]
  }
]);

const App = () => ( <RouterProvider router={router} /> )

export default App
Enter fullscreen mode Exit fullscreen mode

Luego volveremos a este archivo para agregar mas cosas 👀.

📢 Creando las paginas.

Ahora vamos a crear las tres paginas para las rutas que definimos con anterioridad.
Creamos una carpeta nueva src/pages y dentro creamos 3 archivos.

  1. home.tsx

En este archivo solo vamos a listar los datos que vendrán de la API, asi que por el momento solo pondremos lo siguiente:

import { Link } from 'react-router-dom'

export const Home = () => {
    return (
        <>
            <h1>Home</h1>

            <div className="grid">
                <Link to={`/1`} className='user'>
                    <span>username</span>
                </Link>
            </div>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode
  1. createUser.tsx.

Esta pagina es solo para crear nuevos usuarios o sea nueva data. Por lo cual crearemos un formulario. En esta ocasión no voy a usar un estado para controlar el input del formulario, sino simplemente usare el evento que emite el formulario cuando ejecute el onSubmit del mismo (Es importante ponerle el atributo name al input).

export const CreateUser = () => {

    const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault()
        const form = e.target as HTMLFormElement
        const data = Object.fromEntries(new FormData(form))

        // TODO: create new user

        form.reset()
    }

    return (
        <div>
            <h1>Create User</h1>
            <form onSubmit={handleSubmit} className='mt'>
                <input name='user' type="text" placeholder='Add new user' />

                <button>Add User</button>
            </form>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode
  1. editUser.tsx

En esta pagina se editara el usuario seleccionado, obtendremos su ID mediante los parámetros de la URL, tal como lo establecimos cuando creamos el router.

import { useParams } from 'react-router-dom';

export const EditUser = () => {
    const params = useParams()

    const { id } = params

    if (!id) return null

    return (
        <>
            <span>Edit user {id}</span>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

Ahora necesitamos colocar estas paginas en el router!

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { Layout } from './layout';
import { CreateUser } from './pages/createUser';
import { EditUser } from './pages/editUser';
import { Home } from './pages/home';

const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: "/create",
        element: <CreateUser />,
      },
      {
        path: "/:id",
        element: <EditUser />,
      },
    ]
  }
]);

const App = () => (
    <RouterProvider router={router} />
)

export default App
Enter fullscreen mode Exit fullscreen mode

📢 Configurando React Query.

Primero instalaremos la librería.

npm install @tanstack/react-query
Enter fullscreen mode Exit fullscreen mode

Luego configuramos el proveedor en el archivo src/App.tsx

  1. Primero crearemos el queryClient.

Para esta ocasión vamos a dejar estas opciones, que nos ayudaran a usar React Query también como un administrador de estado:

  • refetchOnWindowFocus: Cuando sales de tu app y luego vuelves React Query vuelve hacer la petición de la data.
  • refetchOnMount: Cuando el componente se vuelve a montar entonces volverá a hacer la petición.
  • retry: Número de veces que se volverá a intentar la petición.
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
      refetchOnMount: false,
      retry: 1,
    },
  },
});
Enter fullscreen mode Exit fullscreen mode
  1. Luego necesitamos importar el proveedor que nos ofrece React Query y mandarle el queryClient que acabamos de crear.
const App = () => (
  <QueryClientProvider client={queryClient}>
    <RouterProvider router={router} />
  </QueryClientProvider>
)
Enter fullscreen mode Exit fullscreen mode
  1. Y finalmente, aunque es opcional, pero la verdad es muy muy util, instalaremos las devtools de React Query, que ayudaran mucho.
npm install @tanstack/react-query-devtools
Enter fullscreen mode Exit fullscreen mode

Ahora colocamos las devtools dentro del proveedor de React Query.

  1. El archivo quedaría asi finalmente.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { Layout } from './layout';
import { CreateUser } from './pages/createUser';
import { EditUser } from './pages/editUser';
import { Home } from './pages/home';

const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: "/create",
        element: <CreateUser />,
      },
      {
        path: "/:id",
        element: <EditUser />,
      },
    ]
  }
]);

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnMount: false,
      refetchOnWindowFocus: false,
      retry: 1
    },
  },
});

const App = () => (
  <QueryClientProvider client={queryClient}>
    <ReactQueryDevtools initialIsOpen={false} />
    <RouterProvider router={router} />
  </QueryClientProvider>
)

export default App
Enter fullscreen mode Exit fullscreen mode

📢 Usando React Query como administrador de estado.

Primero crearemos las queryFn que ejecutaremos.

📢 Creando las funciones para hacer las peticiones.

Vamos a crear una carpeta src/api, y crearemos el archivo user.ts, aquí tendremos las funciones para hacer las peticiones, a la API.
Para no demorar mas tiempo en crear una API, usaremos JSON place holder ya que nos permitirá hacer un "CRUD" y no solo peticiones GET.

Crearemos 4 funciones para hacer el CRUD.

Primero establecemos las constantes y la interfaz

La interfaz, es la siguiente:

export interface User {
    id: number;
    name: string;
}
Enter fullscreen mode Exit fullscreen mode

Y las constantes son:

import { User } from '../interface';

const URL_BASE = 'https://jsonplaceholder.typicode.com/users'
const headers = { 'Content-type': 'application/json' }
Enter fullscreen mode Exit fullscreen mode
  1. Primero haremos una función para pedir los usuarios. esta función debe retornar una promesa.
export const getUsers = async (): Promise<User[]> => {
    return await (await fetch(URL_BASE)).json()
}
Enter fullscreen mode Exit fullscreen mode
  1. Luego la función para crear un nuevo usuario, que recibe un usuario y retorna una promesa que resuelve el nuevo usuario.
export const createUser = async (user: Omit<User, 'id'>): Promise<User> => {
    const body = JSON.stringify(user)
    const method = 'POST'
    return await (await fetch(URL_BASE, { body, method, headers })).json()
}
Enter fullscreen mode Exit fullscreen mode
  1. Otra función para editar un usuario, que recibe el usuario a editar y retorna una promesa que resuelve el usuario editado.
export const editUser = async (user: User): Promise<User> => {
    const body = JSON.stringify(user)
    const method = 'PUT'
    return await (await fetch(`${URL_BASE}/${user.id}`, { body, method, headers })).json()
}
Enter fullscreen mode Exit fullscreen mode
  1. Finalmente, una función para eliminar el usuario, que recibe un id. Y como al eliminar un registro de la API, este no regresa nada, entonces regresaremos una promesa que resuelva el id para identificar que usuario fue eliminado.
export const deleteUser = async (id: number): Promise<number> => {
    const method = 'DELETE'
    await fetch(`${URL_BASE}/${id}`, { method })
    return id
}
Enter fullscreen mode Exit fullscreen mode

Asi quedaría este archivo:

import { User } from '../interface';

const URL_BASE = 'https://jsonplaceholder.typicode.com/users'
const headers = { 'Content-type': 'application/json' }

export const getUsers = async (): Promise<User[]> => {
    return await (await fetch(URL_BASE)).json()
}

export const createUser = async (user: Omit<User, 'id'>): Promise<User> => {
    const body = JSON.stringify(user)
    const method = 'POST'
    return await (await fetch(URL_BASE, { body, method, headers })).json()
}

export const editUser = async (user: User): Promise<User> => {
    const body = JSON.stringify(user)
    const method = 'PUT'
    return await (await fetch(`${URL_BASE}/${user.id}`, { body, method, headers })).json()
}

export const deleteUser = async (id: number): Promise<number> => {
    const method = 'DELETE'
    await fetch(`${URL_BASE}/${id}`, { method })
    return id
}
Enter fullscreen mode Exit fullscreen mode

📢 Obteniendo los datos con React Query.

En vez de colocar el código de React Query directamente en el componente, los colocaremos de una vez en un custom hook para tener centralizado nuestro código en un solo lugar.

Asi que crearemos una carpeta src/hook y dentro un archivo llamado useUser.tsx

El primer custom hook que crearemos sera el de useGetUsers el cual solo retorna las propiedades que regresa el hook de useQuery.

Cabe notar que useQuery, necesita de 2 parámetros, un arreglo de strings para identificar la query, y el segundo parámetro la función que hemos realizado anteriormente que es para obtener los usuarios de la API.

import { useQuery } from '@tanstack/react-query';
import { getUsers } from '../api/user';

const key = 'users'

export const useGetUsers = () => {
    return useQuery([key], getUsers);
}
Enter fullscreen mode Exit fullscreen mode

Ahora, toca usar useGetUsers. Como notaras, es lo mismo que si usamos useQuery, pero sin necesitar establecer la queryKey y la queryFn, asiéndolo mas fácil de leer

import { Link } from 'react-router-dom'
import { useGetUsers } from '../hook/useUser'

export const Home = () => {

    const { data, isLoading, isError } = useGetUsers()

    return (
        <>
            <h1>Home</h1>

            {isLoading && <span>fetching a character...</span>}
            {isError && <span>Ups! it was an error 🚨</span>}

            <div className="grid">
                {
                    data?.map(user => (
                        <Link to={`/${user.id}`} key={user.id} className='user'>
                            <span>{user.name}</span>
                        </Link>
                    ))
                }
            </div>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

Hasta aquí, solo hemos establecido la data y guardando los estos datos en la cache (que actuara como nuestra store que almacena el estado), aun no hemos usado/modificado el estado de este componente en algún otro lugar.

📢 Agregando nueva data a nuestro estado.

Vamos a src/hooks/useUser.tsx y crearemos un nuevo custom hook para crear nuevos usuarios.

export const useCreateUser = () => {}
Enter fullscreen mode Exit fullscreen mode

En esta ocasión y en las que siguen, usaremos useMutation ya que vamos a ejecutar una petición de tipo POST, para crear un nuevo registro.

useMutation recibe la queryFn a ejecutar, en este caso le pasaremos la función que creamos para agregar un nuevo usuario.

export const useCreateUser = () => {
    return useMutation(createUser)
}
Enter fullscreen mode Exit fullscreen mode

Le pasaremos un segundo parámetro que sera un objeto, el cual accederemos a la propiedad onSuccess que es una función que se ejecuta cuando la petición ha resultado exitosa.

onSuccess recibe varios parámetros, y usaremos el primero que es la data que regresa la función createUser que en este caso debe ser el usuario nuevo.

export const useCreateUser = () => {

return useMutation(createUser, {
        onSuccess: (user: User) => {}
    })
}
Enter fullscreen mode Exit fullscreen mode

Ahora lo que queremos es acceder a la cache (o sea nuestro estado) y agregar este nuevo usuario creado.

Para esta tarea usaremos otro hook de React Query, useQueryClient

Nota: No desestructures ninguna propiedad del hook useQueryClient porque perderás la referencia y dicha propiedad no funcionara como tu quieres.

Ahora, dentro del cuerpo de la función de onSuccess, vamos a establecer el nuevo dato, mediante la propiedad setQueryData.

setQueryData, necesita 2 parámetros, el primero es la queryKey para identificar que parte de la cache vas a obtener los datos y modificarlos.

export const useCreateUser = () => {
    const queryClient = useQueryClient();

    return useMutation(createUser, {
        onSuccess: (user: User) => {
            queryClient.setQueryData([key])
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

El segundo parámetro es la función para establecer la nueva data. La cual debe recibir por parámetro, la data que ya esta en la cache, que en este caso puede ser un arreglo de usuarios o undefined.

Lo que se hará, sera una validación, donde si ya existen usuarios en la cache, solamente agregamos el nuevo usuario y esparcimos los usuarios anteriores, de lo contrario solo retornamos el usuario creado en un arreglo.

export const useCreateUser = () => {
    const queryClient = useQueryClient();

    return useMutation(createUser, {
        onSuccess: (user: User) => {

            queryClient.setQueryData([key],
                (prevUsers: User[] | undefined) => prevUsers ? [user, ...prevUsers] : [user]
            )

            // queryClient.invalidateQueries([key])
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

Nota: Observa la linea comentada en el código anterior

queryClient.invalidateQueries([key])

Esta linea de código, sirve para invalidar la cache y volver a pedir los datos al servidor. Es lo que normalmente quieres hacer cuando haces algún tipo de petición POST, PUT, DELETE, etc.

En mi caso, comento esta linea, porque la API JSON placeholder no modifica los datos, solo simula hacerlo. Por lo que si yo hago una petición DELETE para eliminar un registro y todo sale bien y luego coloco invalidateQueries pues me regresara de nuevo todos los usuarios y parecerá que no hubo algún cambio en la data.

Una vez aclarado esto, usaremos el custom hook en src/pages/createUser.tsx

Establecemos el custom hook, en este caso, puedes desestructurar las props que retorna este hook pero yo no lo hare solo por gusto. (Aunque cuando usemos mas de un hook, esta sintaxis sera una buena opción para evitar conflicto con los nombres de las props)

    const create_user = useCreateUser()
Enter fullscreen mode Exit fullscreen mode

Ahora en el handleSubmit, accederemos a la propiedad mutateAsync, y gracias a TypeScript sabemos que argumentos debemos pasar, el cual es el nombre del nuevo usuario.

await create_user.mutateAsync({ name: data.user as string  })
Enter fullscreen mode Exit fullscreen mode

Si te preguntas de donde sacamos este argumento, pues es de la función, pues es de la función de crearUsuario del archivo src/>api/user.ts, depende de lo que reciba como parámetro, es lo que enviaremos como argumento.

Y la pagina quedaría de este manera:

import { useCreateUser } from '../hook/useUser'

export const CreateUser = () => {

    const create_user = useCreateUser()

    const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault()
        const form = e.target as HTMLFormElement
        const data = Object.fromEntries(new FormData(form))

        await create_user.mutateAsync({ name: data.user as string })

        form.reset()
    }

    return (
        <div>
            <h1>Create User</h1>
            <form onSubmit={handleSubmit} className='mt'>
                <input name='user' type="text" placeholder='Add new user' />
                {create_user.isLoading && <span>creating user...</span>}
                <button>Add User</button>
                {create_user.isSuccess && <span>User created successfully ✅</span>}
                {create_user.isError && <span>Ups! it was an error 🚨</span>}
            </form>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

📢 Eliminando los datos del estado.

Ahora toca eliminar datos, y los paso son similares que cuando creamos datos.

  • Creamos el custom hook,useDeleteUser.
  • Usamos useMutation, mandando la función a ejecutar, deleteUser.
  • Accedemos a la propiedad onSuccess, para ejecutar la función
  • Usamos useQueryClient para modificar la data en la cache, una vez que sea exitosa la petición.
  • Mandamos la queryKey a la propiedad setQueryData, y el función, validamos si existen datos, en el caso de que si, filtramos la data por el ID que recibimos del onSuccess y excluimos al usuario que acabamos de eliminar, regresando el nuevo arreglo sin el usuario eliminado.
export const useDeleteUser = () => {

    const queryClient = useQueryClient();

    return useMutation(deleteUser, {
        onSuccess: (id) => {
            queryClient.setQueryData([key],
                (prevUsers: User[] | undefined) => prevUsers ? prevUsers.filter(user => user.id !== id) : prevUsers
                // queryClient.invalidateQueries([key])
            )
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Usamos nuestro custom hook en src/pages/editUser.tsx

Pero antes vamos a separar en componentes diferentes las acciones a realizar. Primero crearemos un componente en el mismo archivo, lo nombraremos DeleteUser el cual recibe el id del usuario a eliminar.

import { useParams } from 'react-router-dom';
import { useDeleteUser } from '../hook/useUser';
import { User } from '../interface';

export const EditUser = () => {
    const params = useParams()

    const { id } = params

    if (!id) return null

    return (
        <>
            <DeleteUser id={+id} />
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

DeleteUser tendrá lo siguiente.

Establecemos el custom hook useDeleteUser y accedemos al método mutateAsync para realizar ejecutar la petición y le mandamos el id.

export const DeleteUser = ({ id }: Pick<User, 'id'>) => {
    const delete_user = useDeleteUser()

    const onDelete = async () => {
        await delete_user.mutateAsync(id)
    }

    return (
        <>
            {delete_user.isLoading && <span>deleting user...</span>}

            <button onClick={onDelete}>Delete User</button>

            {delete_user.isSuccess && <span>User deleted successfully ✅, go back home</span>}
            {delete_user.isError && <span>Ups! it was an error 🚨</span>}
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

Y listo, una vez eliminado, regresas a la pagina Home y notaras que se ha eliminado correctamente el usuario. Claro que si refrescas en navegador, este usuario vuelve aparecer porque estamos usando JSON placeholder.

📢 Actualizando los datos del estado.

Ahora toca actualizar un usuario siguiendo los mismos pasos.

  • Creamos el custom hook,useEditUser.
  • Usamos useMutation, mandando la función a ejecutar, editUser.
  • Accedemos a la propiedad onSuccess, para ejecutar la función
  • Usamos useQueryClient para modificar la data en la cache, una vez que sea exitosa la petición.
  • Mandamos la queryKey a la propiedad setQueryData, y el función, validamos si existen datos, en el caso de que si, identificamos el usuario que se modifico mediante el ID y le asignamos su nuevo valor ya modificado.
export const useEditUser = () => {
    const queryClient = useQueryClient();

    return useMutation(editUser, {
        onSuccess: (user_updated: User) => {

            queryClient.setQueryData([key],
                (prevUsers: User[] | undefined) => {
                    if (prevUsers) {
                        prevUsers.map(user => {
                            if (user.id === user_updated.id) {
                                user.name = user_updated.name
                            }
                            return user
                        })
                    }
                    return prevUsers
                }
            )
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

Ahora vamos a src/pages/editUser.tsx y crearemos 2 componentes mas para mostrarte un inconveniente.
Creamos los componentes ViewUser para ver el usuario y EditUser que sera un formulario para editar el usuario.

import { useParams } from 'react-router-dom';
import { useDeleteUser, useEditUser, useGetUsers } from '../hook/useUser';
import { User } from '../interface';

export const EditUser = () => {
    const params = useParams()

    const { id } = params

    if (!id) return null

    return (
        <>
            <ViewUser id={+id} />
            <EditUserForm id={+id} />
            <DeleteUser id={+id} />
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

ViewUser recibe el id, y hace uso del useGetUsers para traer todos los usuarios (lo cual no dispara otra petición, sino que accede a los que están en la cache).
Filtramos el usuario y lo mostramos en pantalla.

export const ViewUser = ({ id }: Pick<User, 'id'>) => {

    const get_users = useGetUsers()

    const user_selected = get_users.data?.find(user => user.id === +id)

    if (!user_selected) return null

    return (
        <>
            <h1>Edit user: {id}</h1>
            <span>User name: <b>{user_selected?.name}</b></span>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

EditUser, también recibe un ID. De hecho este componente es bastante igual al de la pagina createUser.tsx, que incluso pueden reutilizarlo, pero en mi caso no lo hare.

Pasamos a usar el custom hook useEditUser, accedemos a su método mutateAsync y le pasamos los argumentos necesarios. Y listo ya podrás editar el usuario seleccionado.


export const EditUserForm = ({ id }: Pick<User, 'id'>) => {

    const edit_user = useEditUser()

    const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault()
        const form = e.target as HTMLFormElement
        const data = Object.fromEntries(new FormData(form))
        await edit_user.mutateAsync({ name: data.user as string, id })
        form.reset()
    }

    return (
        <>
            <form onSubmit={handleSubmit}>
                <input name='user' type="text" placeholder='Update this user' />
                {edit_user.isLoading && <span>updating user...</span>}
                <button>Update User</button>
                {edit_user.isSuccess && <span>User updated successfully ✅</span>}
                {edit_user.isError && <span>Ups! it was an error 🚨</span>}
            </form>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

Pero cuidado, notaras que cuando un usuario se actualiza correctamente, el componente ViewUser no se renderiza, o sea mantiene el valor del nombre del usuario anterior. Pero si regresas a la pagina Home, notaras que el nombre del usuario si se actualizo.

Esto es debido a que se necesita un nuevo renderizado para que cambien el componente ViewUser.

Para ello se me ocurre una solución. Crear un nuevo custom hook que maneje un observable y estar pendiente de los cambios en cierta parte de la cache.

En este custom hook vamos a usar el otros custom hook useGetUsers y el hook useQueryClient.

  1. Primero usamos el useGetUsers y retornamos sus props, pero sobrescribimos la prop data, ya que es la que tenemos que estar al pendiente de cambios.
export const useGetUsersObserver = () => {

    const get_users = useGetUsers()

    return {
      ...get_users,
        data:[],
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Creamos un estado para manejar el arreglo de usuarios, y ese estado se lo asignamos a la prop data.
export const useGetUsersObserver = () => {

    const get_users = useGetUsers()

    const [users, setUsers] = useState<User[]>()

    return {
        ...get_users,
        data: users,
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Inicializamos el estado con la data existente en la cache, en caso de que no haya datos en la cache retornamos un arreglo vació. Esto lo logramos usando el hook useQueryClient y su propiedad getQueryData
export const useGetUsersObserver = () => {

    const get_users = useGetUsers()

    const queryClient = useQueryClient()

    const [users, setUsers] = useState<User[]>(() => {

        const data = queryClient.getQueryData<User[]>([key])
        return data ?? []
    })

    return {
        ...get_users,
        data: users,
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Ahora si usaremos un efecto para manejar el observador. Dentro creamos una nueva instancia de QueryObserver que requiere dos argumentos, el queryClient y un objeto donde necesita la queyKey para saber que parte de la cache se estará observando.
useEffect(() => {
    const observer = new QueryObserver<User[]>(queryClient, { queryKey: [key] })

}, [])
Enter fullscreen mode Exit fullscreen mode
  1. Ahora necesitamos suscribirnos al observador, por lo que ejecutamos la propiedad subscribe del observer. El subscribe recibe un callback el cual devuelve un objeto que es básicamente las mismas propiedades que devuelve un hook como useQuery por lo que validamos si en la propiedad data existe data, entonces actualizamos el estado con esta nueva data.
useEffect(() => {
    const observer = new QueryObserver<User[]>(queryClient, { queryKey: [key] })

    const unsubscribe = observer.subscribe(result => {
        if (result.data) setUsers(result.data)
    })

}, [])
Enter fullscreen mode Exit fullscreen mode
  1. Recuerda que una buena practica es cancelar la suscripción cuando el componente se desmonte.
useEffect(() => {
    const observer = new QueryObserver<User[]>(queryClient, { queryKey: [key] })

    const unsubscribe = observer.subscribe(result => {
        if (result.data) setUsers(result.data)
    })

    return () => {
        unsubscribe()
    }
}, [])
Enter fullscreen mode Exit fullscreen mode

Y asi quedaría este nuevo custom hook.


export const useGetUsersObserver = () => {

    const get_users = useGetUsers()

    const queryClient = useQueryClient()

    const [users, setUsers] = useState<User[]>(() => {

        const data = queryClient.getQueryData<User[]>([key])
        return data ?? []
    })


    useEffect(() => {
        const observer = new QueryObserver<User[]>(queryClient, { queryKey: [key] })

        const unsubscribe = observer.subscribe(result => {
            if (result.data) setUsers(result.data)
        })

        return () => {
            unsubscribe()
        }
    }, [])

    return {
        ...get_users,
        data: users,
    }
}
Enter fullscreen mode Exit fullscreen mode

Ahora solo es cuestión de usarlo en el componente donde queremos estar al pendiente de estos datos. Como en el componente ViewUser.

No olvides importar useGetUsersObserver

export const ViewUser = ({ id }: Pick<User, 'id'>) => {

    // const get_users = useGetUsers()
    const get_users = useGetUsersObserver()

    const user_selected = get_users.data?.find(user => user.id === +id)

    if (!user_selected) return null

    return (
        <>
            <h1>Edit user: {id}</h1>
            <span>User name: <b>{user_selected?.name}</b></span>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

Ahora si cuando intentes actualizar los datos o eliminarlo, veras como el componente ViewUser también se actualizara una vez la petición sea realizada de manera exitosa.

Y con esto terminaríamos el CRUD usando como gestor de estado la cache de React Query.

📢 Conclusión.

React Query es una librería muy potente que sin duda nos ayuda con el manejo de las peticiones. Pero ahora puedes extenderte mucho mas sabiendo que puedes utilizarlo como un gestor de estado, probablemente una alternativa más.

Espero que te haya gustado esta publicación y que también espero haberte ayudado a extender mas tus conocimientos con React Query.

Si conoces alguna otra forma distinta o mejor de como gestionar el estado usando React Query, con gusto puedes hacerla saber en los comentarios.

Te invito a que revises mi portafolio en caso de que estés interesado en contactarme para algún proyecto! Franklin Martinez Lucas

🔵 No olvides seguirme también en twitter: @Frankomtz361

📢 Demostración simple.

https://rq-state-management.netlify.app/

📢 Código fuente.

https://github.com/Franklin361/state-management-react-query

💖 💪 🙅 🚩
franklin030601
Franklin Martinez

Posted on March 17, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related