home

The URL is a great place to store state in React

March 11, 2025

Sometimes, the best place to store state is right in the URL. It’s simple, practical, and often overlooked. Let’s explore why it’s worth considering.

The Problem

Here’s what we want to achieve: Let’s say we have a modal (dialog) component that allows the user to perform some important actions which are part of the core flow. We would like the modal to stay open even after the user reloads the page.

If we were to just use useState for the modal open state, we would lose this information when the page reloads. Let’s see how we can solve this problem.

Before and After

Ways to Store State on the Client

Let’s look at the options we have to store state to achieve the desired behavior. The options can generally be categorized as follows:

In the Application

Data that belongs in the working memory of the application. Lasts as long as the application is running.

const App = () => {
    const [open, setOpen] = useState(false);
    return <Modal open={open} onClose={() => setOpen(false)} />;
};

In the Browser

Data that is stored in the browser. Examples include localStorage and sessionStorage.

const App = () => {
    const [open, setOpen] = useState(() => {
        try {
            return localStorage.getItem("modalOpen") === "true";
        } catch (error) {
            console.error("Failed to read from localStorage:", error);
            return false; // Default fallback value
        }
    });

    useEffect(() => {
        try {
            localStorage.setItem("modalOpen", open);
        } catch (error) {
            console.error("Failed to write to localStorage:", error);
        }
    }, [open]);

    return <Modal open={open} onClose={() => setOpen(false)} />;
};

In the Server

Data that is stored in the server and accessible via an API. This method would require the client to make a request to the server each time the state of the modal changes.

const App = () => {
    const [open, setOpen] = useState(false);
    const [isOpen, setIsOpen] = useState(false);

    useEffect(() => {
        const updateModalState = async () => {
            try {
                const response = await fetch("/api/modal", {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify({ open: isOpen }),
                });

                if (!response.ok) {
                    throw new Error(`API error: ${response.status}`);
                }

                const data = await response.json();
                if (data.modalState !== undefined) {
                    setOpen(data.modalState);
                }

            } catch (error) {
                console.error("Failed to update modal state:", error);
            }
        };

        updateModalState();
    }, [isOpen]);

    return (
        <Modal
            open={open}
            onClose={() => setIsOpen(false)}
        />
    );
};

In the URL

This is the encoded data stored in the URL in the form of a string. Good examples are Query Parameters ("/users?userId=desc") and Path Parameters ("/users/1324").

Why choose the URL

As with everything, there are pros and cons to each approach.

Pros

Cons

How to store state in the URL

Storing state in the URL involves using query parameters (?key=value) or hash fragments (#section). This allows state persistence across page reloads and enables users to share specific application states via URLs.

In React, useSearchParams from React Router or the URLSearchParams API can be used to manage query parameters dynamically.

Using useSearchParams (React Router)

import { useSearchParams } from "react-router-dom";

function Example() {
    const [searchParams, setSearchParams] = useSearchParams();
    const updateParam = () => {
        setSearchParams({ filter: "active" });
    };

    return (
        <div>
            <button onClick={updateParam}>Set Filter</button>
            <p>Current Filter: {searchParams.get("filter")}</p>
        </div>
    );
}

This ensures state is reflected in the URL (?filter=active) and persists across reloads.

Using URLSearchParams (Vanilla JS)

const params = new URLSearchParams(window.location.search);
params.set("theme", "dark");
window.history.replaceState({}, "", "?" + params.toString());

This manually updates the URL without causing a page reload.

If your needs are a little more complex, then third-party utilities like nuqs are great options for managing URL state in React with type-safety.

import { useQueryState } from "nuqs";

function Example() {
    const [filter, setFilter] = useQueryState("filter");
    const updateParam = () => {
        setFilter("active");
    };

    return (
        <div>
            <button onClick={updateParam}>Set Filter</button>
            <p>Current Filter: {filter}</p>
        </div>
    );
}

Conclusion

Choosing where to store state depends on its purpose and how it interacts with the application. Query parameters (?key=value) are ideal in specific scenarios where state persistence, shareability, and browser behavior are important.

Use Query Parameters (or Path Parameters) When:

Use useState When:

Use Local Storage (or Session Storage) When: