DEV Community

loading...

Using URL as a global state - React Hook

viglioni profile image Laura Viglioni ・4 min read

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.

Alt Text

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 :)

Alt Text

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 */
Enter fullscreen mode Exit fullscreen mode

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 */
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

(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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

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:

Alt Text

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"
}
Enter fullscreen mode Exit fullscreen mode

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 :)

Discussion

pic
Editor guide