How to handle routing in a ReScript React app using pattern matching.
Tags: rescript, react, routing, vite
Published: 2023-09-24 20:04:59

_Photo by <a href="https://unsplash.com/@alschim?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Alexander Schimmeck</a> on <a href="https://unsplash.com/photos/Aohf8gqa7Zc?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>_
  
Most React applications have multiple pages and you need to set up some type of routing. Let's take a look at how to add multiple pages to a React application created with ReScript and Vite.

We'll be starting with a basic React app created with ReScript, Vite, and Tailwindcss. [This guide will help you get that set up](https://dev.to/jderochervlk/how-to-set-up-a-react-app-using-rescript-vite-and-tailwind-for-lightning-fast-development-oa).

[`@rescript/react`](https://github.com/rescript-lang/rescript-react) comes with a router already so we don't need to install anything else to get started. [Here are the full docs](https://rescript-lang.org/docs/react/latest/router), but let's walk through each step in detail to make sure we understand it.

## Create two page components
We'll need to create a component for each page we want to be able to navigate to. Let's make a page called `dog` and one called `cat`. We're using Tailwind for our styles.
```rescript
// src/Dog.res
@react.component
let make = () => {
  <div>
    <h2 className="text-xl"> {React.string("Dog")} </h2>
    <img
      className="max-w-full mx-auto mt-5"
      alt="a dog"
      src="https://media.giphy.com/media/4Zo41lhzKt6iZ8xff9/giphy.gif"
    />
  </div>
}
```

```rescript
// src/Cat.res
@react.component
let make = () => {
  <div>
    <h2 className="text-xl"> {React.string("Cat")} </h2>
    <img
      className="max-w-sm mx-auto mt-5"
      alt="a cat"
      src="https://media.giphy.com/media/7YCA4XbA0ArQuWEfqJ/giphy.gif"
    />
  </div>
}

```

You will also want to create an interface file (`.resi`) for each of these components to make sure Vite's HMR works correctly and to speed up the already fast ReScript compiler. If you are using VSCode with the ReScript extension you can do this automatically by hitting `f1` and looking for `ReScript: Create an interface file for this implementation file`. It's always good practice to create `.resi` files.
```rescript
// src/Dog.resi
@react.component
let make: unit => React.element
```

```rescript
// src/Cat.resi
@react.component
let make: unit => React.element
```

> Note: [`unit`](https://rescript-lang.org/docs/manual/latest/primitive-types#unit) means the function doesn't take in any parameters.

## Creating a Page component to handle what to render
We'll need a `Page` component to handle rendering the correct content based on the route.

This will use ReScript's [Pattern Matching](https://rescript-lang.org/docs/manual/latest/pattern-matching-destructuring#sidebar) feature to handle what we want to show based on the route. We'll `switch` on the `url.path`, which is a `list`. In our case we want to render `<Dog/>` if the list has one item that is `"dog"`, which would be a path of `/dog`. If the path had multiple parts, like `/dog/poodle/fluffy` we would have `#list{"dog", "poodle", "fluffy"}`. For now let's just focus on handling "dog" and "cat".

```rescript
@react.component
let make = () => {
  let url = RescriptReactRouter.useUrl()

  switch url.path {
  | list{"dog"} => <Dog />
  | list{"cat"} => <Cat />
  | _ => <h2> {React.string("404: page not found")} </h2>
  }
}
```

Remember to generate the `.resi` file for this component!

## Add the Page component to `App.res`
Clear out the contents of the existing `src/App.res` file and set it up to render our new Page component.

```rescript
@react.component
let make = () =>
  <div className="text-center p-5 text-gray-700">
    <h1 className="text-3xl m-5"> {React.string("Cats or Dogs?")} </h1>
    <Page />
  </div>
```

You should now see our 404 error. 
![404 screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pgt9k6nm35ftx8259olr.png)

If you manually add `/cats` to the url you should see the cat page.

![a cat](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kgyhsdo35dm8lgwdtpya.png)

## Creating our navigation bar
We'll need a quick nav bar to move between pages:
```rescript
// src/Nav.res
@react.component
let make = () => {
  <nav className="flex justify-center space-x-2 w-full mx-auto">
    <button className="bg-blue-300 p-2 mx-1 rounded text-white"> {React.string("cat")} </button>
    <button className="bg-blue-300 p-2 mx-1 rounded text-white"> {React.string("dog")} </button>
  </nav>
}
```

Add it to `App.res`:
```rescript
@react.component
let make = () =>
  <div className="text-center p-5 text-gray-700">
    <h1 className="text-3xl m-5"> {React.string("Cats or Dogs?")} </h1>
    <Nav />
    <Page />
  </div>
```

Now we have buttons, but they don't do anything, and we start out on a 404 page.

## Hooking it all up.
### Redirect to a default page
If a user opens our app we don't want them to see a 404 page, so let's redirect them to "/cat".

We can get the current url from `RescriptReactRouter.useUrl()` and then switch on it inside of a [`useEffect`](https://rescript-lang.org/docs/react/latest/hooks-effect) hook. `url.path` is a [`list`](https://rescript-lang.org/docs/manual/latest/array-and-list#list) and we can pattern match for an empty list and replace the url with `cat` or do nothing by returning `()` (that's how you write `unit`).

Here's our new code:
```rescript
  let url = RescriptReactRouter.useUrl()

  React.useEffect0(() => {
    switch url.path {
    | list{} => RescriptReactRouter.replace("cat")
    | _ => ()
    }
    None
  })
```

You'll notice we used `useEffect0` instead of `useEffect`. In ReScript a value can only have one type so we need different functions depending on how many values we plan on using in the dependency array. We only want to do this once so we would be passing it an empty array if we were in normal JavaScript, but here we just use `useEffect0`. If we had one item we would use `useEffect1` and so on.

ReScript forces us to always return something as part of cleaning up a `useEffect` hook, and since we don't have anything to clean up here we just use `None` as the last expression.

Here's what `App.res` looks like now:
```rescript
@react.component
let make = () => {
  let url = RescriptReactRouter.useUrl()

  React.useEffect0(() => {
    switch url.path {
    | list{} => RescriptReactRouter.replace("cat")
    | _ => ()
    }
    None
  })

  <div className="text-center p-5 text-gray-700">
    <h1 className="text-3xl m-5"> {React.string("Cats or Dogs?")} </h1>
    <Nav />
    <Page />
  </div>
}
```
### Set up navigation
We need to show a new page after a user clicks on a navigation button.

Let's make on function to handle clicking on a navigation button:
```rescript
  let handleClick = React.useCallback0(path => {
    RescriptReactRouter.push(path)
  })
```

`.push` will change our URL in a way that allows the browsers back button to still work and it will stay in sync with the `useUrl` hook we have in the `Page.res` component.

We can call this from each navigation button with the correct path.
```rescript
// src/Nav.res
@react.component
let make = () => {
  let handleClick = React.useCallback0(path => {
    RescriptReactRouter.push(path)
  })

  <nav className="flex justify-center space-x-2 w-full mx-auto">
    <button onClick={_ => handleClick("cat")} className="bg-blue-300 p-2 mx-1 rounded text-white">
      {React.string("cat")}
    </button>
    <button onClick={_ => handleClick("dog")} className="bg-blue-300 p-2 mx-1 rounded text-white">
      {React.string("dog")}
    </button>
  </nav>
}
```

You now have a small app with functioning navigation!

Please let me know in the comments if you have any questions.