DEV Community

Cover image for Managing state with React Query. 〽️
Franklin Martinez
Franklin Martinez

Posted on

Managing state with React Query. 〽️

React Query is a large and complete library that facilitates the work when making client-side requests to the server and even performs much more than that.

But Did you know that you can use this library as a state manager?, possibly an alternative to redux-toolkit, zustand, among others. In this article I will show you how to implement it this way.

🚨 Note: to understand this article you should have basic knowledge of how to use React Query and also some basic knowledge with TypeScript.

 

Table of contents.

📌 Technologies to be used.

📌 Creating the project.

📌 First steps.

📌 Creating the pages.

📌 Configuring React Query.

📌 Using React Query as status manager.

📌 Creating the functions to make the requests.

📌 Getting the data with React Query.

📌 Adding new data to our state.

📌 Removing data from the state.

📌 Updating the status data.

📌 Conclusion.

📌 Demonstration.

📌 Source code.

 

📢 Technologies to be used.

  • 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 (You can find the styles in the repository at the end of this post)

📢 Creating the project.

We will name the project: state-management-rq (optional, you can name it whatever you like).

npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

We create the project with Vite JS and select React with TypeScript.

Then we run the following command to navigate to the directory just created.

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

Then we install the dependencies.

npm install
Enter fullscreen mode Exit fullscreen mode

Then we open the project in a code editor (in my case VS code).

code .
Enter fullscreen mode Exit fullscreen mode

📢 First steps.

First we are going to install a dom router to be able to create a couple of pages in our app.

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

So, let's create a src/layout folder to create a very simple navigation menu that will be on all the pages.
Inside src/layout we create the index.tsx file and add the following:

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

Then in the src/App.tsx file we are going to delete everything. And we are going to create our basic routes.

Note: We are going to set the routes using createBrowserRouter, but if you want you can use the components that react-router-dom still has like <BrowserRouter/>, <Routes/>, <Route/>, etc. instead of createBrowserRouter.

By createBrowserRouter we are going to create an object where we will add our routes. Note that I only have a parent route, and what I show is the navigation menu, and this route has 3 daughter routes, which for the moment have not been created their pages.

Finally we create the component App that we export by default, this component is going to render a component of react-router-dom that is the <RouterProvider/> that receives the router that we have just created.

And with this we can navigate between the different routes.

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

Then we will come back to this file to add more stuff 👀.

📢 Creating the pages.

Now we are going to create the three pages for the paths we defined earlier.
Create a new folder src/pages and inside create 3 files.

  1. home.tsx

In this file we are only going to list the data that will come from the API, so for the moment we will only put the following:

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.

This page is only for creating new users or new data. So we will create a form. In this occasion I am not going to use a state to control the input of the form, but simply I will use the event that emits the form when it executes the onSubmit of the same one (It is important to put the attribute name to the 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

In this page the selected user will be edited, we will obtain his ID by means of the parameters of the URL, as we established it when we created the 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

Now we need to place these pages in the 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

📢 Configuring React Query.

First we will install the library.

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

Then we configure the provider in the src/App.tsx file.

  1. First we will create the queryClient.

For this occasion we are going to leave these options, that will help us to use React Query also as a state manager:

  • refetchOnWindowFocus: When you exit your app and then come back React Query returns to make the request for the data.
  • refetchOnMount: When the component is remounted then it will make the request again.
  • retry: Number of times to retry the request.
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
      refetchOnMount: false,
      retry: 1,
    },
  },
});
Enter fullscreen mode Exit fullscreen mode
  1. Then we need to import the provider that offers us React Query and send it the queryClient we just created.
const App = () => (
  <QueryClientProvider client={queryClient}>
    <RouterProvider router={router} />
  </QueryClientProvider>
)
Enter fullscreen mode Exit fullscreen mode
  1. And finally, although it is optional, but it is very very useful, we will install the React Query devtools, which will help a lot.
npm install @tanstack/react-query-devtools
Enter fullscreen mode Exit fullscreen mode

Now we place the devtools inside the React Query provider.

  1. The file would finally look like this.
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

📢 Using React Query as status manager.

First we will create the queryFn that we will execute.

📢 Creating the functions to make the requests.

We are going to create a folder src/api, and we will create the file user.ts, here we will have the functions to make the requests, to the API.
In order not to take more time to create an API, we will use JSON place holder because it will allow us to make a "CRUD" and not only GET requests.

**We will create 4 functions to do the CRUD.

First we set the constants and the interface

The interface is as follows:

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

And the constants are:

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. First we will make a function to request the users. this function must return a promise.
export const getUsers = async (): Promise<User[]> => {
    return await (await fetch(URL_BASE)).json()
}
Enter fullscreen mode Exit fullscreen mode
  1. Then the function to create a new user, which receives a user and returns a promise that resolves the new user.
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. Another function to edit a user, which receives the user to edit and returns a promise that resolves the edited user.
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. Finally, a function to delete the user, which receives an id. And since when deleting a record from the API, it does not return anything, then we will return a promise that resolves the id to identify which user was deleted.
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

This is how this file would look like:

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

📢 Getting the data with React Query.

Instead of placing the React Query code directly in the component, we will place them all at once in a custom hook to have our code centralized in one place.

So we will create a folder src/hook and inside a file called useUser.tsx.

The first custom hook we will create will be useGetUsers which only returns the properties returned by the useQuery hook.

Note that useQuery, needs 2 parameters, an array of strings to identify the query, and the second parameter is the function that we have done previously which is to get the users from the 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

So far, we have only set the data and stored this data in the cache (which will act as our store that stores the state), we have not yet used/modified the state of this component elsewhere.

📢 Adding new data to our state.

Let's go to src/hooks/useUser.tsx and create a new custom hook to create new users.

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

In this occasion and in the following ones, we will use useMutation because we are going to execute a POST request to create a new record.

useMutation receives the queryFn to execute, in this case we will pass it the function we created to add a new user.

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

We will pass a second parameter which will be an object, which will access the onSuccess property which is a function that is executed when the request is successful.

onSuccess receives several parameters, and we will use the first one which is the data returned by the createUser function which in this case must be the new user.

export const useCreateUser = () => {

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

Now what we want to do is to access the cache (our state) and add this newly created user.

For this task we will use another React Query hook, useQueryClient.

🚨 Note: Do not destructure any property of the hook useQueryClient because you will lose the reference and this property will not work as you want.

Now, inside the body of the onSuccess function, let's set the new data, using the setQueryData property.

setQueryData, needs 2 parameters, the first one is the queryKey to identify which part of the cache you are going to get the data and modify it.

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

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

The second parameter is the function to set the new data. Which must receive by parameter, the data that is already in the cache, which in this case can be an array of users or undefined.

What will be done, will be a validation, where if there are already users in the cache, we only add the new user and spread the previous users, otherwise we only return the user created in an array.

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

🚨 Note: Observe the line commented in the previous code

queryClient.invalidateQueries([key])

This line of code is used to invalidate the cache and re-request the data from the server. This is what you normally want to do when you make some kind of POST, PUT, DELETE, etc. request..

In my case, I comment this line, because the JSON placeholder API does not modify the data, it only simulates it. So if I make a DELETE request to delete a record and everything goes well and then I put invalidateQueries, it will return all the users again and it will seem that there was no change in the data.

Once this is clear, we will use the custom hook in src/pages/createUser.tsx.

We set the custom hook, in this case, you can unstructure the props returned by this hook but I won't do it just for fun (although when we use more than one hook, this syntax will be a good option to avoid conflict with the names of the props).

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

Now in the handleSubmit, we will access the mutateAsync property, and thanks to TypeScript we know what arguments we must pass, which is the name of the new user.

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

If you wonder where we get this argument, it is from the function, it is from the createUser function of the file src/>api/user.ts, it depends on what it receives as parameter, it is what we will send as argument.

And the page would look like this:

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

📢 Removing data from the state.

Now it is time to delete data, and the steps are similar to when we create data.

  • We create the custom hook, useDeleteUser.
  • We use useMutation, sending the function to execute, deleteUser.
  • We access to the onSuccess property, to execute the function.
  • We use use useQueryClient to modify the data in the cache, once the request is successful.
  • We send the queryKey to the setQueryData property, and the function, we validate if there is data, if yes, we filter the data by the ID we received from the onSuccess and exclude the user we just deleted, returning the new array without the deleted user.
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

We use our custom hook in src/pages/editUser.tsx.

But before we are going to separate in different components the actions to be performed. First we will create a component in the same file, we will name it DeleteUser which receives the id of the user to delete.

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 will have the following.

We set the custom hook useDeleteUser and access the mutateAsync method to execute the request and send it the 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

And that's it, once removed, go back to the Home page and you will notice that the user has been removed correctly. Of course, if you refresh the browser, this user reappears because we are using JSON placeholder.

📢 Updating the status data.

Now it is time to update a user following the same steps.

  • We create the custom hook, useEditUser.
  • We use useMutation, sending the function to execute, editUser.
  • We access to the onSuccess property, to execute the function.
  • We use use useQueryClient to modify the data in the cache, once the request is successful.
  • We send the queryKey to the setQueryData property, and the function, we validate if data exists, if yes, we identify the user that was modified through the ID and we assign its new value already modified.
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

Now let's go to src/pages/editUser.tsx and create 2 more components to show you a drawback.
We create the components ViewUser to see the user and EditUser which will be a form to edit the user.

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 receives the id, and makes use of useGetUsers to fetch all users (which does not trigger another request, but accesses those in the cache).
We filter the user and display it on screen.

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, it also receives an ID. In fact this component is quite the same as the one in the createUser.tsx page, you can even reuse it, but in my case I won't do it.

We use the custom hook useEditUser, we access to its method mutateAsync and we pass the necessary arguments. And ready you will be able to edit the selected user.


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

But be careful, you will notice that when a user is updated correctly, the ViewUser component is not rendered, that is, it keeps the value of the previous user's name. But if you go back to the Home page, you will notice that the user's name is updated.

This is because a new rendering is needed to change the ViewUser component.

For this I can think of a solution. Create a new custom hook that handles an observable and be aware of the changes in a certain part of the cache.

In this custom hook we are going to use the other custom hook useGetUsers and the hook useQueryClient.

  1. First we use the useGetUsers and return its props, but we overwrite the prop data, since it is the one that we have to be aware of changes.
export const useGetUsersObserver = () => {

    const get_users = useGetUsers()

    return {
      ...get_users,
        data:[],
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. We create a state to manage the user array, and we assign that state to the 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. We initialize the state with the existing data in the cache, in case there is no data in the cache we return an empty array. This is achieved using the hook useQueryClient and its property 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. Now we will use an effect to handle the observer. Inside we create a new instance of QueryObserver that requires two arguments, the queryClient and an object where it needs the queyKey to know which part of the cache will be watched.
useEffect(() => {
    const observer = new QueryObserver<User[]>(queryClient, { queryKey: [key] })

}, [])
Enter fullscreen mode Exit fullscreen mode
  1. Now we need to subscribe to the observer, so we execute the subscribe property of the observer. The subscribe receives a callback which returns an object that is basically the same properties that returns a hook like useQuery so we validate if in the data property there is data, then we update the state with this new 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. Remember that a good practice is to cancel the subscription when the component is disassembled.
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

And this is how this new custom hook would look like.


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

Now it is only a question of using it in the component where we want to be aware of this data. As in the ViewUser component.

Don't forget to import 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

Now if when you try to update the data or delete it, you will see how the ViewUser component will also be updated once the request is successful.

And with this we would finish the CRUD using as state manager the React Query cache.

📢 Conclusion.

React Query is a very powerful library that certainly helps us with request handling. But now you can extend it much more knowing that you can use it as a status manager, probably one more alternative.

I hope you liked this post and I also hope I helped you to extend your knowledge with React Query.

If you know any other different or better way on how to manage status using React Query, feel free to let me know in the comments.

I invite you to check my portfolio in case you are interested in contacting me for a project!. Franklin Martinez Lucas

🔵 Don't forget to follow me also on twitter: @Frankomtz361

📢 Demo.

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

📢 Source code.

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

Top comments (3)

Collapse
 
flash010603 profile image
Usuario163

I really didn't know you could do that with react query. 😵‍💫

Collapse
 
raafacachoeira profile image
Rafael Cachoeira

Really nice article exploring the advanced use of this library. Using this library brings many advantages such as performance, but it brings a lot of complexity, expensive configurations and also, large coupling to the lib.

Collapse
 
josh_claunch profile image
Josh Claunch

This is cool. React Query is a server state manager that can be used for UI state too.

My company just open-sourced a tool that takes the opposite approach - it's a UI state manager that can be used for server state too. Not a full replacement for React Query, but it would handle the specific examples in this article much more cleanly.

May be worth checking out if you're interested in this sort of hybrid tooling. GitHub Repo here