For a french version of this article, go here :)
The state of a react app
Managing the global state of a React app is an endless war between libraries. Therefore, in order to add my 2 cents, we'll see how to do the exact same thing using the URL and react-router.
URL FTW
In single page application, the URL does not really matter. Most of the time, it is just an endpoint to request all the assets and that's it.
Landing on https://myApp.io
ou https://myApp.io?user=gael&job=dev
would have no impact on what you'll see on the first loading of the app.
Let's change that.
Some code
I started to use develop this idea in a side project (code and demo to play with :))
However, for this article, I have redeveloped a codesandbox to focus only on what matters for here.
First of all, how will we use the URL ?
Well, we'll use everything located after the ? in the URL, the so-called search params.
Some docs here on the subject.
Get values from the URL
In the context of this article, we are going to look for only one parameter that we'll name query.
To retrieve that parameter (if it is indeed in the URL eg. https://myApp.io?query=javascript
), we are going to check the ... search params. Luckily, they are easy to find, located in the object window
. More precisely in winndow.location.search
.
Therefore, if we request: www.first-contrib?query=react
, what we get in the console is:
console.log(window.location.search); // "?query=react"
Ideally, rather than a string
, it would be more convenient to manipulate a JS object properly formatted. To achieve that, instead of splitting the URL by =
and ?
, we'll use the URLSearchParams
object, available in recent browsers. Otherwise, you can polyfill it thanks to this lib for instance.
With code, it yields to :
function getParams(location) {
const searchParams = new URLSearchParams(location.search);
return {
query: searchParams.get('query') || '',
};
}
therefore,
const params = getParams('www.first-contrib.fr?query=react');
console.log(params) // { query: "react" }
Now that we can get an object out of the URL, we'll make it work with our app which uses react-router. We'll therefore create a router
which handles a route
getting the properties in the URL as props
.
import React from "react";
import { render } from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";
// ...
// getParams code above
//a simple component to display
//the value of the query ...
// which is for now unknown
//so we'll instantiate it with an empty value
const MainPage = (props) => {
let query = '';
return (
<h2>{`Query : ${query}`}</h2>
);
}
const App = () => (
<React.Fragment>
<Router>
<React.Fragment>
<Route path="/" component={MainPage} />
</React.Fragment>
</Router>
</React.Fragment>
);
render(<App />, document.getElementById("root"));
To get the actual value of query
, we'll have to manipulate the function getParams
that we wrote as well as the implicit props
that MainPage
receives from the Route
object : <Route path="/" component={MainPage} />
If we logged that props
, we'd get :
{match: Object, location: Object, history: Object, /*other stuff */}
What is of interest here is the location
object similar to the window.location
we manipulated earlier. Therefore, we can update the MainPage
component to get the value inside the URL:
const MainPage = (props) => {
const { location } = props;
const { query } = getParams(location);
return (
<h2>{`My query: ${query}`}</h2>
);
}
Now MainPage
is plugged to the URL !
Update the URL (and the state) !
Now that we can read from the URL, we are going to implement a way to update the URL accordingly to the state of our app.
For that, a simple input
will do:
class InputPage extends React.Component {
state = { inputValue: "" };
updateInputValue = e => this.setState({ inputValue: e.target.value });
render() {
return (
<React.Fragment>
<input
type="text"
placeholder="Change your URL !"
value={this.state.inputValue}
onChange={this.updateInputValue}
/>
<input type="button" value="Change the URL" onClick={null} />
</React.Fragment>
);
}
}
So far, our component manage an internal state to display its current value but we still have to implement the onClick
function to update the URL with that very same value.
We saw that the implicit props
object from Route
looks like that:
{match: Object, location:Object, history: Object, /*d'autres valeurs */}
What is of interest here is the history
(additional information about history
here...)
Thanks to its push
function which according to React Router documentation :
Pushes a new entry onto the history stack
To say it simply, push
will allow us to update the URL !
So if the query
value in our input is javascript
, we'll have to update our URL to the following value: www.myApp.io?query=javascript
. We therefore need to generate the new searchParams
of our URL. In order to do that URLSearchParams
object will help us once again:
function setParams({ query = ""}) {
const searchParams = new URLSearchParams();
searchParams.set("query", query);
return searchParams.toString();
}
Mind that without a default value for query when query is actually
undefined
, the generated URL would be?query=undefined
...
Now we can write :
const url = setParams({ query: "javascript" });
console.log(url); // "query=javascript"
We can then implement the onClick
in he input component.
class InputPage extends React.Component {
state = { inputValue: "" };
updateInputValue = e => this.setState({ inputValue: e.target.value });
updateURL = () => {
const url = setParams({ query: this.state.inputValue });
//do not forget the "?" !
this.props.history.push(`?${url}`);
};
render() {
return (
<React.Fragment>
<input
type="text"
className="input"
placeholder="What am I looking for ?"
value={this.state.inputValue}
onChange={this.updateInputValue}
/>
<input
type="button"
className="button"
value="Update the URL !"
onClick={this.updateURL}
/>
</React.Fragment>
);
}
}
Now if we change that value of the input, a click on the button we'll trigger the update of the URL and the MainPage
will display the new value accordingly !
One of the nice thing that comes by having the state of your app in a URL is when you copy/paste link. Given that the state is included in this URL, we'll find the app in a specific state on first load !
When you deal with a search engine for instance, you can trigger the query as soon as the app is loaded. In this application, I use react-apollo but in naive way, we can implement the same thing with any HTTP client.
Let's create a component which triggers request using axios and the Github REST API (which does not require any authentication) as soon as it gets props
using some of its lifecycle methods.
const httpClient = axios.create({
baseURL: "https://api.github.com"
});
class ResultsPage extends React.Component {
state = { results: [], loading: false, error: false };
//Search as soon as it is mounted !!
componentDidMount() {
return this.searchRepositories(this.props.query);
}
//Search as soon as query value is updated
componentWillReceiveProps(nextProps) {
if (nextProps.query !== this.props.query) {
this.setState({ query: nextProps.query });
return this.searchRepositories(nextProps.query);
}
}
searchRepositories = query => {
//handle if query is undefined
if (!query) {
return this.setState({
results: []
});
}
this.setState({ loading: true, error: false });
//the actual search on Github
return httpClient
.get(`/search/repositories?q=${query}`)
.then(({ data }) =>
this.setState({
results: data.items,
loading: false
})
)
.catch(e => this.setState({ loading: false, error: true }));
};
render() {
return (
<div>
{this.state.results.map(repo => (
<div key={repo.id}>
<a href={repo.html_url}>
{repo.name}
</a>
<div>{`by ${repo.owner.login}`}</div>
</div>
))}
</div>
);
}
}
That's it! We now have a component which triggers request whenever the query
params contained in the URL is updated !
As mentionned earlier, you can find a live example here.
In our example, we deal with only one searchParams
but it becomes really cool and powerful if more components can modify the URL. For instance, pagination, filtering, sorting, etc can become parameters of the URL. It could then look like that: https://myApp.io?query=react&sort=ASC&filter=issues&page=2
.
The code would be similar as what we did previously. By modifying the URL, implicit props
provided by the Route
component are updated. It then triggers a rerender and update all the children listening to a specific value in the URL. Therefore it leads to UI update(s) or side effect(s) such as an HTTP call.
Conclusion
That's it ! This article was an attempt to show that there are still alternatives to handle the global state of a React App that can be very light in terms of bundle size (0kb in modern browser ><) ans still fun, simple and providing for free a (kind of) deep linking effect which I find very cool :)
Hope you enjoy !
Top comments (16)
Thank you @GealS for this. The example for search is a great way to use query params (QPs).
In terms of more advanced component patterns though, do you have any use cases where you can optimize performance/minimize loading by using query params?
By using QPs, can you somehow mitigate the almost endless return to previous routes that's triggered by
history
when users click the back button? I'm assuming that QPs allow you update UI without actual route changes? So does this translate to history only going back to route changes and not query changes?Regarding point #2:
History.replaceState(...) may solve your problem:
Hi, thanks for the feedback :)
i'll try to answer the 2 points you raised :
Despite the use of the QPs, as I see it, optimizing the loading would mean optimizing the request over the network. So far, I only come up with general ideas (which come regardless QPs) such as caching, smart prefetching (= the request being sent before the user even hits enter), and things like that.
As I understand your question here, to save bandwidth and improve UX, I think caching the requests' responses could be - again-the solution here. With QPs, going back in history means querying something you already have so, better use a cache (which comes for free with react-apollo for instance).
Thanks a lot for your reply, especially on caching. I liked the comment earlier and was hoping to get back to it.
Other than when you use react-apollo, which other libraries do you use for caching in non Apollo projects?
With classic rest API, there are several strategies to deal with caching.
1) You can have a REST API providing a
cache-Control
in its HTTP headers to help the browser to minimize server round-trips when possible (an interested link on the subject). It's a good one since few to no work is required on the front-end side.2) You can have a reverse proxy on your back-end to filter incoming requests and use cache when possible. You do not save bandwidth but at least, you release the server's load.
3) However, if you are not in charge of the API but you still want to have a caching method, a naive way of doing it is to use
memoization
. Just a big word to describe afunction
that can save the results of its computation. Therefore, if it's called twice with the same arguments, the second call costs no compute time.You can achieve this using closure in
javascript
.Let's say you use axios to query whatever API you want with an
id
as an argument (mind that it's kind of a pseudo code - it won't run as is) :Obviously, it's a very very very naive implementation which would require additional work to deal with expire dates, invalidation of the cache if a mutation is sent to the server for instance and so on. But I hope you'll get the idea of how the subject could be tackled :)
I definitely do. This has sure come in handy, and just cycling back to say thank you.
long shot but react-query is the go-to now ;)
Hey man! This article is awesome, I am currently learning React. I have a problem, I have a list page, and a detail page, when I use history.goBack() on the detail page to return to the list page, the list page is refreshed and does not maintain the previous state. I found a solution through your article. Can I translate your article, learn, and then put it on the China Programming Forum website like Stack Overflow? Website Addr: segmentfault.com/
Sure thing ! Awesome that it helps you ! :)
Thanks Gael, Really enjoyed your walkthrough - delightful and enlightening! btw love your miami vice styling!
Hello Sir, I have gone thru your demo search app. UI looks very nice. I have also created same app for one of my project, now the issue is if I do couple of searches and go back the search result doesnot change though the url param changes. This same issue I have seen in your app too. Any idea how to resolve this
Hey, I translated your article into Chinese. This is the article link: segmentfault.com/a/1190000019364821 , thank you again!
Awesome ! :D
Hi, this is very helpful with my search form back and forth. How do you pass multiple query parameters? I have search form with one input textbox, one checkbox, now I can only update one parameter, either input box or checkbox, not both, ie. myapi/?keyword=test&checkbox=null. How to update state for multiple params in setParams function because you only can get one component state? Thanks.
URLs are a great strategy
Can use existing library for this npmjs.com/package/state-in-url , for don't reinvent the wheel.