Skip to content

How do I add routing to a SAFE app with a shared model for all pages?

Written for SAFE template version 4.2.0

When building larger apps, you probably want different pages to be accessible through different URLs. In this recipe, we show you how to add routes to different pages to an application, including adding a "page not found" page that is displayed when an unknown URL is entered.

In this recipe we use the simplest approach to storing states for multiple pages, by creating a single state for the full app. A potential benefit of this approach is that the state of a page is not lost when navigating away from it. You will see how that works at the end of the recipe.

1. Adding the Feliz router

Install Feliz.Router in the client project

dotnet paket add Feliz.Router -p Client -V 3.8

Feliz.Router versions

At the time of writing, the current version of the SAFE template (4.2.0) does not work well with the latest version of Feliz.Router (4.0). To work around this, we install Feliz.Router 3.8, the latest version that works with SAFE template version 4.2.0.

If you are working with a newer version of the SAFE template, it might be worth trying to install the newest version of Feliz.Router. To see the installed version of the SAFE template, run in the command line:

dotnet new --list

To include the router in the Client, open Feliz.Router at the top of Index.fs

open Feliz.Router

2. Adding the URL object

Add the current page to the model of the client, using a new Page type

type Page =
    | TodoList
    | NotFound

type Model =
    { CurrentPage: Page
      Todos: Todo list
      Input: string }
+ type Page =
+     | TodoList
+     | NotFound
+
- type Model = { Todos: Todo list; Input: string }
+ type Model =
+    { CurrentPage: Page
+      Todos: Todo list
+      Input: string }

3. Parsing URLs

Create a function to parse a URL to a page, including a wildcard for unmapped pages

let parseUrl url = 
    match url with
    | ["todo"] -> Page.TodoList
    | _ -> Page.NotFound

4. Initialization when using a URL

On initialization, set the current page

let init () : Model * Cmd<Msg> =
    let page = Router.currentUrl () |> parseUrl

    let model =
        { CurrentPage = page
          Todos = []
          Input = "" }
    ...
    model, cmd
  let init () : Model * Cmd<Msg> =
+     let page = Router.currentUrl () |> parseUrl
+
-      let model = { Todos = []; Input = "" }
+      let model =
+        { CurrentPage = page
+         Todos = []
+         Input = "" }
      ...
      model, cmd

5. Updating the URL

Add an action to handle navigation.

To the Msg type, add a PageChanged case of Page

type Msg =
    ...
    | PageChanged of Page
 type Msg =
     ...
+    | PageChanged of Page

Add the PageChanged update action

let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
    match msg with
    ...
    | PageChanged page -> { model with CurrentPage = page }, Cmd.none
  let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
      match msg with
      ...
+     | PageChanged page -> { model with CurrentPage = page }, Cmd.none

6. Displaying the correct content

Rename the view function to todoView

let todoView (model: Model) (dispatch: Msg -> unit) =
    Bulma.hero [
    ...
    ]
- let view (model: Model) (dispatch: Msg -> unit) =
+ let todoView (model: Model) (dispatch: Msg -> unit) =
      Bulma.hero [
      ...
      ]

Add a new view function, that returns the appropriate page

let view (model: Model) (dispatch: Msg -> unit) =
    match model.CurrentPage with
    | TodoList -> todoView model dispatch
    | NotFound -> Bulma.box "Page not found"

Adding UI elements to every page of the website

In this recipe, we moved all the page content to the todoView, but you don't have to. You can add UI you want to display on every page of the application to the view function.

7. Adding the React router to the view

Add the React.Router element as the outermost element of the view. Dispatch the PageChanged event on onUrlChanged

let view (model: Model) (dispatch: Msg -> unit) =
    React.router [
        router.onUrlChanged (parseUrl >> PageChanged >> dispatch)
        router.children [
            match model.CurrentPage with
            ...
        ]
    ]
  let view (model: Model) (dispatch: Msg -> unit) =
+     React.router [
+         router.onUrlChanged (parseUrl >> PageChanged >> dispatch)
          router.children [
              match model.CurrentPage with
              ...
          ]
      ]

9. Try it out

The routing should work now. Try navigating to localhost:8080; you should see a page with "Page not Found". If you go to localhost:8080/#/todo, you should see the todo app.

To see how the state is maintained even when navigating away from the page, type something in the text box and move away from the page by entering another path in the address bar. Then go back to the todo page. The entered text is still there.

# sign

You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh. There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

10. Adding more pages

Now that you have set up the routing, adding more pages is simple: add a new case to the Page type; add a route for this page in the parseUrl function; add a function that takes a model and dispatcher to generate your new page, and add a new case to the pattern match inside the view function to display the new case.