Sometimes in small applications using some state manager like Redux can be a little pain in the *, with this post I hope to help with a less spartan way to achieve this.
For this litle POC we will use ReactJS, NextJS and Dog Ceo Api
The main purpose of this solution is to avoid creating a state in a parent component and keep passing it and its setter as props to the children components.
In this example we have two components: a Home
in pages/index/index.js
and some buttons in pages/components/breed-buttons
.
You can checkout full code at my github page and see it running here :)
Our Home
component have a state called breed with "random" as its default value and this component makes an API call to get a random picture of a dog. Usually we would do something like:
const Home = () => {
const [breed, setBreed] = useState("random")
/* api call */
/* display pic */
Our BreedButtons
component is a simple div
with some buttons with breed names that when clicked set our breed state with its respective breed value. For this to be possible, we must pass breed
and setBreed
as props:
const Home = () => {
const [breed, setBreed] = useState("random")
/* api call */
<BreedButtons breed={breed} setBreed={setBreed}/>
/* display pic */
Now just picture the scene when Home
has a lot of children that might read or write this state. And you have more states. It might get messy.
In our helpers/hooks.js
you will find this React Hook:
(obs.: if you do not know pathOr
you should checkout ramda, it is an amazing functional lib!)
This function gets two parameters: first the name of this state and second the initial value. For instance, in our problem we want a state called breed with default value random. The use is very similar to React's useState
:
const [breed, setBreed] = useRouterAsState("breed", "random")
Our hook will check if our URL has already some value for the state "breed", for instance https://global-state-example.herokuapp.com/?breed=husky
, if yes it will set the state to "husky" or whatever it is placed after the equal sign, if not to our default value "random".
Whenever we change the state in any component, i.e.
setBreed("dalmatian")
the next router
will change in URL to /?breed=dalmatian
and all components the used our useRouteAsState
will automatically update its value. If we have more states in our URL it will change only the "breed" state. That is why you name the state in useRouteAsState
first parameter.
This is what our Home
looks like:
where getDog
is a syntax sugar for our API call in helpers/api.js
. Our BreedButtons
component contains the buttons that actually change our state and it looks like this:
Of course our approach has a lot of limitations such as: it simply does not make sense for some states to be in URL like loading or data, but it is very usefull to states like pagination, dark-mode etc.
Be careful, it might get messy if more than one component tries to change the state at the same time, so you must use it very carefully when thinking about concurrency.
On the other hand, more than just being handy not to have to pass the same state and setter to a lot of children, grandchildren again and again, the state in URL has a huge advantage of not losing context when the page is reloaded or when you hit back button - in mobile web development this behaviour is fundamental.
Another important point: sometimes you don't want to give the power of manually setting a state to the user just typing in the URL, specially if some of the states are being used in an API call. For this problem we have a partial solution.
If you check our pages/encoded/
code you will see that the components are very similar to our pages/index
ones, except that our hook is imported from helpers/encoded-state.js
. The code is a little bit longer so I won't print it here, but the main difference is instead of our URL be something like ?breed=labrador
it will be ?c3RhdGVz=eyJicmVlZCI6ImxhYnJhZG9yIn0%253D
. It is a Base64 encoding of
state: {
breed: "labrador"
}
The user will be able to decode it, but it is a little bit safer than just let the state in plain text.
A last but not least point: you might have noticed that in our useRouteAsState
we have a third parameter called r
. In some older versions of NextJS
the useRouter
native hook does not work, so you must import withRouter
from next/router
and wrap your component using it, for example: export default withRouter(Home)
with that you will receive a prop called router
in Home
component that is our third paramter.
I really hope you find this little trick useful. Any doubts or sugestions you can call me on my twitter account twitter.com/viglionilaura :)
Top comments (1)
There are few packages for syncing state and URL, for example npmjs.com/package/state-in-url .