A router component for React and Elmish that is focused, powerful and extremely easy to use.
Here is a full example in Feliz
module App
open Feliz
open Feliz.Router
[<ReactComponent>]
let Router() =
let (currentUrl, updateUrl) = React.useState(Router.currentUrl())
React.router [
router.onUrlChanged updateUrl
router.children [
match currentUrl with
| [ ] -> Html.h1 "Index"
| [ "users" ] -> Html.h1 "Users page"
| [ "users"; Route.Int userId ] -> Html.h1 (sprintf "User ID %d" userId)
| otherwise -> Html.h1 "Not found"
]
]
open Browser.Dom
let root = ReactDOM.createRoot(document.getElementById "root")
root.render(Router())
Full Elmish example
module App
open Feliz
open Feliz.Router
type State = { CurrentUrl : string list }
type Msg = UrlChanged of string list
let init() = { CurrentUrl = Router.currentUrl() }
let update (UrlChanged segments) state = { state with CurrentUrl = segments }
let render state dispatch =
React.router [
router.onUrlChanged (UrlChanged >> dispatch)
router.children [
match state.CurrentUrl with
| [ ] -> Html.h1 "Home"
| [ "users" ] -> Html.h1 "Users page"
| [ "users"; Route.Int userId ] -> Html.h1 (sprintf "User ID %d" userId)
| _ -> Html.h1 "Not found"
]
]
Program.mkSimple init update render
|> Program.withReactSynchronous "root"
|> Program.run
dotnet add package Feliz.Router
The package includes a single React component called router
that you can use at the very top level of your application
React.router [
router.onUrlChanged (UrlChanged >> dispatch)
router.children [
Html.h1 "App"
]
]
Where it has two primary properties
router.onUrlChanged : string list -> unit
gets triggered when the url changes where it gives you the url segments to work with.router.children: ReactElement
the element to be rendered as the single child of therouter
component, usually here is where your root applicationrender
function goes.router.children: ReactElement list
overload to be rendered as the children of therouter
let currentPage = Html.h1 "App"
React.router [
router.onUrlChanged (UrlChanged >> dispatch)
router.children [
Html.div [
Html.h1 "Using the router"
currentPage
]
]
]
Routing in most applications revolves around having your application react to url changes, causing the current page to change and data to reload. Here is where router.onUrlChanged
comes into play where it triggers when the url changes giving you the cleaned url segments as a list of strings. These are sample urls their corresposing url segments that get triggered as input of of onUrlChanged
:
segment "#/" => [ ]
segment "#/home" => [ "home" ]
segment "#/home/settings" => [ "home"; "settings" ]
segment "#/users/1" => [ "users"; "1" ]
segment "#/users/1/details" => [ "users"; "1"; "details" ]
// with query string parameters
segment "#/users?id=1" => [ "users"; "?id=1" ]
segment "#/home/users?id=1" => [ "home"; "users"; "?id=1" ]
segment "#/users?id=1&format=json" => [ "users"; "?id=1&format=json" ]
Instead of using overly complicated parser combinators to parse a simple structure such as URL segments, the Route
module includes a handful of convenient active patterns to use against these segments:
type Page =
| Home
| Users
| User of id:int
| NotFound
// string list -> Page
let parseUrl = function
// matches #/ or #
| [ ] -> Page.Home
// matches #/users or #/users/ or #users
| [ "users" ] -> Page.Users
// matches #/users/{userId}
| [ "users"; Route.Int userId ] -> Page.User userId
// matches #/users?id={userId} where userId is an integer
| [ "users"; Route.Query [ "id", Route.Int userId ] ] -> Page.User userId
// matches everything else
| _ -> NotFound
[<ReactComponent>]
static member Router() =
let (pageUrl, updateUrl) = React.useState(parseUrl(Router.currentUrl()))
let currentPage =
match pageUrl with
| Home -> Html.h1 "Home"
| Users -> Html.h1 "Users page"
| User userId -> Html.h1 (sprintf "User ID %d" userId)
| NotFound -> Html.h1 "Not Found"
React.router [
router.onUrlChanged (parseUrl >> updateUrl)
router.children currentPage
]
Of course, you can define your own patterns to match against the route segments, just remember that you are working against simple string.
Aside from listening to manual changes made to the URL by hand, the React.router
element is able to listen to changes made programmatically from your code with Router.navigate(...)
.
To use this function is as a command inside your update
function the same functionality is exposed as Cmd.navigate
.
The function Router.navigate
(and Cmd.navigate
) has the general syntax:
Router.navigate(segment1, segment2, ..., segmentN, [query string parameters], [historyMode])
Examples of the generated paths:
Router.navigate("users") => "#/users"
Router.navigate("users", "about") => "#/users/about"
Router.navigate("users", 1) => "#/users/1"
Router.navigate("users", 1, "details") => "#/users/1/details"
Examples of generated paths with query string parameters
Router.navigate("users", [ "id", 1 ]) => "#/user?id=1"
Router.navigate("users", [ "name", "john"; "married", "false" ]) => "#/users?name=john&married=false"
// paramters are encoded automatically
Router.navigate("search", [ "q", "whats up" ]) => @"#/search?q=whats%20up"
// Pushing a new history entry is the default bevahiour
Router.navigate("users", HistoryMode.PushState)
// to replace current history entry, use HistoryMode.ReplaceState
Router.navigate("users", HistoryMode.ReplaceState)
In addition to Router.navigate(...)
you can also use the Router.format(...)
if you only need to generate the string that can be used to set the href
property of a link.
The function Router.format
has a similar general syntax as Router.navigate
:
Router.format(segment1, segment2, ..., segmentN, [query string parameters])
Examples of the generated paths:
Router.format("users") => "#/users"
Router.format("users", "about") => "#/users/about"
Router.format("users", 1) => "#/users/1"
Router.format("users", 1, "details") => "#/users/1/details"
Examples of generated paths with query string parameters
Router.format("users", [ "id", 1 ]) => "#/user?id=1"
Router.format("users", [ "name", "john"; "married", "false" ]) => "#/users?name=john&married=false"
// paramters are encoded automatically
Router.format("search", [ "q", "whats up" ]) => @"#/search?q=whats%20up"
Example of usage:
Html.a [
prop.href (Router.format("users", ["id", 10]))
prop.text "Single User link"
]
The router by default prepends all generated routes with a hash sign (#
), to omit the hash sign and use plain old paths, use router.pathMode
React.router [
router.pathMode
router.onUrlChanged (parseUrl >> PageChanged >> dispatch)
router.children currentPage
]
Then refactor the application to use path-based functions rather than the default functions which are hash-based:
Hash-based | Path-based |
---|---|
Router.currentUrl() |
Router.currentPath() |
Router.format() |
Router.formatPath() |
Router.navigate() |
Router.navigatePath() |
Cmd.navigate() |
Cmd.navigatePath() |
Using (anchor) Html.a
tags using path mode can be problematic because they cause a full-refresh if they are not prefixed with the hash sign.
Still, you can use them with path mode routing by overriding the default behavior using the prop.onClick
event handler to dispatch a
message which executes a Cmd.navigatePath
command. It goes like this:
type Msg =
| NavigateTo of string
let update msg state =
match msg with
| NavigateTo href -> state, Cmd.navigatePath(href)
let goToUrl (dispatch: Msg -> unit) (href: string) (e: MouseEvent) =
// disable full page refresh
e.preventDefault()
// dispatch msg
dispatch (NavigateTo href)
let render state dispatch =
let href = Router.format("some-sub-path")
Html.a [
prop.text "Click me"
prop.href href
prop.onClick (goToUrl dispatch href)
]
Here is a full example in an Elmish program.
type State = { CurrentUrl : string list }
type Msg =
| UrlChanged of string list
| NavigateToUsers
| NavigateToUser of int
let init() = { CurrentUrl = Router.currentUrl() }, Cmd.none
let update msg state =
match msg with
| UrlChanged segments -> { state with CurrentUrl = segments }, Cmd.none
// notice here the use of the command Cmd.navigate
| NavigateToUsers -> state, Cmd.navigate("users")
// Router.navigate with query string parameters
| NavigateToUser userId -> state, Cmd.navigate("users", [ "id", userId ])
let render state dispatch =
let currentPage =
match state.CurrentUrl with
| [ ] ->
Html.div [
Html.h1 "Home"
Html.button [
prop.text "Navigate to users"
prop.onClick (fun _ -> dispatch NavigateToUsers)
]
Html.a [
prop.href (Router.format("users"))
prop.text "Users link"
]
]
| [ "users" ] ->
Html.div [
Html.h1 "Users page"
Html.button [
prop.text "Navigate to User(10)"
prop.onClick (fun _ -> dispatch (NavigateToUser 10))
]
Html.a [
prop.href (Router.format("users", ["id", 10]))
prop.text "Single User link"
]
]
| [ "users"; Route.Query [ "id", Route.Int userId ] ] ->
Html.h1 (sprintf "Showing user %d" userId)
| _ ->
Html.h1 "Not found"
React.router [
router.onUrlChanged (UrlChanged >> dispatch)
router.children currentPage
]
The 3.x release refactored the API to be more in line with how Feliz libraries are built as well as using latest features from React like functional components to implement the router itself. This release also made it easy to work with the router from a React-only applications and not just from Elmish based apps.
To migrate your router to latest version, here are the required changes:
Router.router
becomesReact.router
Router.onUrlChanged
becomesrouter.onUrlChanged
Router.application
becomesrouter.children
Router.navigate
becomesCmd.navigate
for the Elmish variant andRouter.navigate() : unit
for the React variant
The rest of the implementation and API is kept as is. If you have any questions or run into problems, please let us know!
Starting from Feliz.Router v4, it will only work with Fable compiler v4+ and Feliz 2.x+
If you are using Fable compiler v3, then you should use Feliz.Router v3.10 (latest) which still relies on Feliz v1.68 for Fable v3 compatibility.
# start by installing dependencies
npm install
# run tests
npm test
# run the demo application in watch mode
npm run start:demo
# build the demo application
npm run build:demo