Use Case
You have a webpage showing your data, and an input component that controls the filtering of that data.
For example, you have a simple list of students, and a search box that filters the list as you type.
You want, of course, to make the value
of the input reactive, in order to change the subset of the data whenever the value
changes.
But you also want the user to be able to share a link to the current status of the page at any time.
In our example, you want the contents of the search box to be represented in the URL as well, as Query String, for example.
Let's see how to make the value
from one component to be reactive to both component state and Router, with clean code.
The Simple (but complicated) Way
We will start with the direct way. We need to get the data from the route
, and update the route on change:
We have a few APIs (hooks) to get the data from the Router. For this post I chose to use URL Params with
useParams
for simpler code samples, although in a real app, Query String make more sense for this kind of use case.Note that to use URL Params you have to declare the params in the Router
path
prop, something like/:param1/:param2?
.
Get the data from the route
When the component mounts, we need to read the URL Params, in case the user gets to our component from a link that should affect the state:
const SearchBox = () => {
const { param1 } = useParams()
const [search, setSearch] = useState(param1)
}
But this is not enough, since route changes often don't reload the page (which is good). If there is no page reload, the state won’t change because the component is already mounted.
We need to define the URL Param change as an effect:
const SearchBox = () => {
const { param1 } = useParams()
const [search, setSearch] = useState()
useEffect(() => setSearch(param1), [param1])
}
✔️ Getting the data from the route
is done - the state is synced with the route (but the route is not synced with the state).
Update the route on change
Now we can update the search
state with setSearch
, but we want to keep the URL up to date with the latest search
, in order to allow the user to copy the URL at any time.
The only way (that I know) to change the URL with React Router is with the history
API:
const SearchBox = () => {
// Code from previous examples
return (
<div>
<input
type="text"
value={search}
onChange={(e) => history.replace("/" + e.target.value)}
/>
<SearchList search={search} />
</div>
);
};
✔️ Update the route on change is done - change the route
instead of the state
, and the state
will get updated from the useEffect
hook.
Interim conclusions
- It is working very well in our simple example (even better than I thought!)
- To use the
route
as ourstate
, we used four hooks (useParams
,useState
,useHistory
anduseEffect
) instead of one hook to get thestate
and a method to update it. - We will need more and more code if we want to use Query Params or if we want the
history.replace
function call to be more generic with thepath
argument.
Actually, the solution seems very simple at this point.
Spoiler- I created a package that implements this solution.
I must share that I wonder if I would have created this package if I had written the article earlier, and understood that the solution is simple and required.
In any case, I felt a lack of idea-sharing of this solution, so I think it is worth writing the post and creating the package.
We need to hide all this logic in a custom hook.
Organize the code
Let's move all the code to a dedicated function:
const useCustomParams = () => {
const { param1 } = useParams();
const [search, setSearch] = useState();
const history = useHistory();
useEffect(() => setSearch(param1), [param1]);
const replace = (newParam) => history.replace("/" + newParam);
return [search, replace];
};
You don't really need the
state
here, because you have a closed flow fromuseParams
to the component, then withhistory
back to theuseParams
.
But you will need it for the Query String case.
All that is left is not to be dependent on the path
or specific URL Param:
const useCustomParams = () => {
const { params, path } = useRouteMatch();
const history = useHistory();
const updateParams = (updatedParams) => {
Object.assign(params, updatedParams);
history.push(generatePath(path, params));
};
return [params, updateParams];
};
I don't know the specific path
or params
, I just take, update and push them again.
After going through this process myself, and seeing that there was a lack of information on this subject, I created an npm package called use-route-as-state
that implements the solution described in the article.
You are welcome to use and contribute!
Thanks to @brafdlog for linguistic editing and suggestions.
Top comments (2)
is it okay to do this performace wise ? in some cases we could be making heavy api calls to servers.
Why? In which case it will make redundant API calls?
If you're using it as a state, your component will work the same, and only the URL updating will be different.