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.