I’m building an e-commerce page using React that includes various options stored in state, such as sortBy
, itemsPerPage
, and several interdependent categories. I want these state values to also be reflected in the URL as search parameters.
I’m using React-Router’s useSearchParams
hook to keep the state and URL in sync. However, I’m encountering issues with useEffect
dependencies and potential logical flaws due to intentionally omitting certain values from the dependency array to avoid circular dependencies.
I created a custom hook called useURLSync
so that different state can sync and share the same searchParams
object returned by the useSearchParams
hook and to hide all the synchronization logic.
import { useState, useEffect } from "react";
export default function useURLSync(
searchParams,
setSearchParams,
paramName,
type = "string",
initialValue = ""
) {
// Type validation
if (type === "array" && !Array.isArray(initialValue)) {
throw new Error(
`useURLSync: initialValue must be an array when type is "array"`
);
}
if (type === "string" && typeof initialValue !== "string") {
throw new Error(`
useURLSync: initialValue must be a string when type is "string"`);
}
// Intiliaze state from URL search params according to the paramName if it exists,
// if not, return the initial value
const [state, setState] = useState(() => {
if (searchParams.has(paramName)) {
const paramValue = searchParams.get(paramName);
if (paramValue) {
if (type === "string") {
return paramValue;
} else if (type === "array") {
return paramValue.split(",");
}
}
}
return initialValue;
});
// Update the URL when state changes
useEffect(() => {
// Create a new URL search params object from a copy of the current one
const newSearchParams = new URLSearchParams(searchParams);
let shouldChange = false;
if (state.length > 0) {
const currentParamValue = searchParams.get(paramName);
if (type === "array") {
const newParamValue = state.join(",");
if (newParamValue !== currentParamValue) {
newSearchParams.set(paramName, newParamValue);
shouldChange = true;
}
} else if (type === "string") {
const newParamValue = state;
if (newParamValue !== currentParamValue) {
newSearchParams.set(paramName, newParamValue);
shouldChange = true;
}
}
} else if (newSearchParams.has(paramName)) {
newSearchParams.delete(paramName);
shouldChange = true;
}
if (shouldChange) {
setSearchParams(newSearchParams, { replace: true });
}
}, [state]);
// Update state when URL seach params changes
useEffect(() => {
const paramValue = searchParams.get(paramName);
let newStateValue = initialValue;
if (paramValue) {
if (type === "array") {
newStateValue = paramValue.split(",");
} else if (type === "string") {
newStateValue = paramValue;
}
if (JSON.stringify(newStateValue) !== JSON.stringify(state)) {
setState(newStateValue);
}
} else if (state !== initialValue) {
setState(initialValue);
}
}, [searchParams]);
return [state, setState];
}
And here is how it is used:
// Search Params
const [searchParams, setSearchParams] = useSearchParams();
// Page State
const [page, setPage] = useURLSync(
searchParams,
setSearchParams,
"page",
"string",
"1"
);
// PerPage state
const [perPage, setPerPage] = useURLSync(
searchParams,
setSearchParams,
"perPage",
"string",
"12"
);
// Sort state
const [sort, setSort] = useURLSync(
searchParams,
setSearchParams,
"sort",
"string",
"alpha-desc"
);
const [selectedPlatforms, setSelectedPlatforms] = useURLSync(
searchParams,
setSearchParams,
"platforms",
"array",
[]
);
In the first useEffect
, I update the URL whenever the React state changes. I include the state in the dependency array. Within this useEffect
, I also use the searchParams
object returned by useSearchParams
to get the current parameter value and check if a change is necessary. I intentionally omit searchParams
from the dependency array to avoid a circular dependency.
In the second useEffect
, I update the state
whenever the URL search parameters change. Here, I include searchParams
in the dependency array but omit the state
, even though I use it for comparison.
Is intentionally omitting dependencies in useEffect
the right way to prevent circular updates? How can I properly synchronize the state
and URL search parameters without causing circular updates or omitting necessary dependencies?
2
Answers
In first
useEffect
you only need to update the URL when the state changes, so it’s correct that you’re including state as a dependency here. but removingsearchParams
from the dependency array can give you lots of issues. So, you could optimize the situation here by checking whether the current search params are different from the state and updating only if necessary.Only update the URL when there is an actual change (when the new value is different from the current one). You avoid unnecessary re-renders this way,
useEffect
you should add both state andsearchParam
in the dependency array.useEffect
you are already syncing the url with state based on thesearchParams
. Since you are directly updating the state there, you don’t need to include state in dependency array.This approach should work:
No, you generally want to include all external references in the effect’s dependency array to ensure the effect runs with the latest/current dependency values. This avoids stale closures, etc. If you haven’t already, it is recommended you add eslint-plugin-react-hooks to your project to help highlight missing React hook dependencies.
One thing you could do would be to use the functional updates. This generally eliminates the "state" as a dependency for state updaters, e.g.
setState(currentState => { /* logic to compute next state */ });
and similarly with thesetSearchParams
, using the functional update eliminatessearchParams
as an external dependency, e.g.setSearchParam(searchParams => { /* logic to compute next searchParams */ });
.Overall though I think you’ve overcomplicated your logic.
You basically need:
initialValue
.useEffect
hook to "react" to URLSearchParams updates and update the internal stateuseEffect
hook to "react" to state updates and update the URLSearchParams.Your code can be simplified to:
Alternative
Note that is is generally considered a bit of an anti-pattern to duplicate state or store derived state into local React state. In this case I’d suggest simply returning the current URLSearchParam parameter value and a stable callback handler to update the URLSearchParams.
Example: