DEV Community

Cover image for Simplified State Management with useRQState: A Better Alternative to useState
Nelson Odo
Nelson Odo

Posted on • Edited on

Simplified State Management with useRQState: A Better Alternative to useState

Imagine this: you're building a React app, and suddenly, state management spirals out of control. You find yourself passing props down through a labyrinth of components, a process affectionately known as "prop drilling." It's like playing a game of telephone, but instead of whispers, you're passing data through multiple layers of components, and by the time it reaches the intended recipient, we all know how that game ends – with a big mess!

But fear not, dear React developers! There's a new kid on the block, and its name is useRQState (React Query State). This nifty hook is built on top of React Query and is designed to make local state management a breeze.

This powerful hook elegantly sidesteps the prop-drilling menace by allowing you to access and update state across your entire app using a single, magical query key.

How useRQState Works:

  1. Define Your State: Use useRQState to initialize your state, just like you would with useState.
  2. Assign a Query Key: Give your state a unique identifier, the "query key."
  3. Access and Update: From any component, access and update the state using the same query key. It's like having a secret handshake that only components with the same key understand.

What's the Big Deal About useRQState?

Glad you asked! Here are just a few reasons why useRQState is about to become your new best friend:

  • Prop Drilling? Pshaw! Access and update state anywhere in your app without the dreaded prop-drilling.
  • State Persistence Across Pages: Your state remains steadfast even as you navigate through your app, like a loyal companion.
  • Simplicity Itself: useRQState mimics the familiar useState API, making it easy to adopt without a steep learning curve.

useRQState Implementation

import { useQuery, UseQueryResult } from  "@tanstack/react-query";
import { queryClient } from  "Utils/reactquery";

export function useRQState<T>(queryKey: string, initialData?: T | ((...args: any[]) => T)): 
[ T, (update: Partial<T> | ((prevState: T) => Partial<T>)) => void, Omit<UseQueryResult<T>, "data"> ] 
{
  const { data, ...others } = useQuery({
    queryKey: [queryKey],
    initialData: () => {
      if (initialData === undefined) {
        const storedData = getQueryCache<T>(queryKey, true);
        return storedData;
      }

      if (typeof initialData === 'function') {
        const val = (initialData as CallableFunction)();
        return val;
      }

      return initialData;
    },
    refetchInterval: false,
    refetchOnReconnect: false,
    refetchIntervalInBackground: false,
  });

  function setData(update: Partial<T> | ((prevState: T) => Partial<T>)) {
    queryClient.setQueryData([queryKey], (prevState: T | undefined) => {
      if (typeof update === 'function') {
        return (update as (prevState: T) => Partial<T>)(prevState as T);
      }
      return update;
    });
  }


  return [data, setData, { ...others }];
}

Enter fullscreen mode Exit fullscreen mode

getQueryCache

const getQueryCache = <T>(queryKey: string, exact = false) => {
  const [itemCache] = queryClient.getQueriesData<T>({
    exact,
    queryKey: [queryKey],
  });

  const [_queryKeys, data] = itemCache ?? [];

  return data;
};
Enter fullscreen mode Exit fullscreen mode

Utils/reactquery.ts

import { QueryClient } from "@tanstack/react-query";

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: Infinity,
      refetchOnMount: false,
      refetchOnWindowFocus: false,
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Let's See It in Action!

import { useRQState } from "Hooks/useRQState"; 

function MyComponent() {
  const [counter, setCounter] = useRQState("counter", 0); 

  return (
    <button onClick={() => setCounter(count + 1)}>
      Increment (with a touch of magic!)
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, we're using the query key "counter" to manage our count state. Any component that uses this key can access and update the count, regardless of its position in the component tree.

Updating State with Previous Values

Like useState, useRQState supports updating state based on the previous value, making it perfect for scenarios where you need to increment counters or manage complex state updates:

<button
  onClick={() => setCounter((prev) => prev + 1)}
  className="px-4 py-2 rounded-md bg-green-600 text-white flex-1"
>
  Increment by 1
</button>
Enter fullscreen mode Exit fullscreen mode

Ready to Ditch Prop Drilling?

With useRQState, managing state in your React app becomes a breeze. Say goodbye to spaghetti code and hello to a clean, maintainable, and scalable state management solution.

Want to see it in action? Check out our live demo or head over to the GitHub repo for the source code.

Demo & Source Code πŸ“‚

GitHub Repository: https://github.com/Nedum84/use-rq-state
Live Demo: https://nedum84.github.io/use-rq-state


Let's Connect 🌐


What Do You Think? πŸ’­

Share your thoughts! You can improve the code to serve your use case. Did this post help you?
Have ideas or feedback to share? Let’s continue the conversation in the comments below! 😊

Top comments (3)

Collapse
 
dayoolacodes profile image
Dayo Ayoola • Edited

Impressive work! I have curious questions tho:

  • Won’t you need to wrap your app with QueryClientProvider at the root for this to work, or does it work out of the box without it?
  • Can this be combined with other React Query features like invalidateQueries or useMutation for advanced use cases?
Collapse
 
thenelson_o profile image
Nelson Odo • Edited

I'm really happy you found it useful! 😊

  • Yes, you will need to wrap your app with QueryClientProvider for it to work. This is demonstrated in the source code here: github.com/Nedum84/use-rq-state.
  • The hook exposes all the other useQuery methods, including isLoading, refetch, etc., so you can use features like invalidateQueries. However, it doesn't currently support useMutation, but creating a new hook around it is definitely possible!
  const [count, setCount, { refetch, error, isFetching, ...others}] = useRQState("counter", 0);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
eioluseyi profile image
EIO β€’ Emmanuel Imolorhe

This is just brilliant!