Skip to content

Solid Router

Bindings for @solidjs/router 2, in the Partas.Solid.Router namespace of the Partas.Solid package. Install the npm package yourself:

npm install @solidjs/router@2.0.0-next.38

The Solid 2 router is configured, not composed. There are no Router, Route or A components: you declare the route tree as data, create the router once, and render the router it returns. Links are plain a elements.

Routes and the router

route declares a route. Its component is a component that takes RouteSectionProps, or unit, such as a [<SolidComponent>] let binding. createRouter takes the tree and returns the router.

open Partas.Solid
open Partas.Solid.Router

[<SolidComponent>]
let Home () = h1 () { "Home" }

[<SolidComponent>]
let User (props: RouteSectionProps) =
    h1 () { "User " + (props.``params``["id"] |> Option.defaultValue "") }

[<SolidComponent>]
let Users (props: RouteSectionProps) =
    section () {
        h2 () { "Users" }
        props.children
    }

let Router =
    createRouter (
        RouterConfig(
            [| route ("/", Home)
               route ("/users", Users, children = [| route ("/:id", User, matchFilters = {| id = intFilter |}) |])
               route ("*404", Home) |]
        )
    )

Create the router at module level, and give it a capitalised name, in a capitalised module: the router is the component you render, and the Solid compiler treats a lowercase tag as an HTML element. The plugin reports a lowercase one as an error.

A parent with a component wraps its children, which render where it places props.children. routePaths matches several paths with one route, and routeGroup groups children under a path without a component of its own.

route argument
pathThe path. :id is a parameter, :id? an optional one, *rest the remainder of the path
componentThe component to render
childrenNested routes
preloadRuns on navigation and link preloading. Its result reaches the component as props.data
matchFiltersAn object of filters per parameter. intFilter accepts integers, an array lists the allowed values
infoMetadata, read back through useRouteMatches

RouterConfig takes the routes, then history (browserHistory () by default, hashHistory (), or memoryHistory "/start" for tests), base, preload, links, preloadLinks and the router's other options.

Rendering the router

Router.render () renders the matched route. To keep a layout mounted around it, render the router with a RouterRoot, whose child lambda receives the matched content as props.children:

[<SolidComponent>]
let App () =
    Router % RouterRoot() {
        yield fun props ->
            div () {
                nav () {
                    a (href = "/") { "Home" }
                    a (href = "/users/1") { "User 1" }
                }
                main () { props.children }
            }
    }
export function App() {
    return <Router>
        {(props) => <div>
            <nav>
                <a href="/">
                    Home
                </a>
                <a href="/users/1">
                    User 1
                </a>
            </nav>
            <main>
                {props.children}
            </main>
        </div>}
    </Router>;
}

The hooks below read the router's context, so call them in a component rendered under the router: a route, or a component inside the root layout. App itself is outside it.

The router handles clicks on any same-origin a. Anchor attributes change how it navigates:

Attribute
replace = trueReplace the history entry instead of pushing one
noscroll = trueDo not scroll to the top
state = "..."A JSON string, pushed as location.state
preload = "false"Never preload this link. "viewport" or "eager" opts it into that strategy
link = trueMarks a router link when explicitLinks = true

The router marks links for CSS: aria-current="page" on an exact match, data-active on the link's path and the paths under it, and data-pending on the target of an in-flight navigation when the router is created with links = pendingLinks. useLinkState gives the same state to code.

Link preloading is off by default. Turn it on with preloadLinks = [| intentPreload () |] (hover, focus and touch), tapPreload (), viewportPreload () or eagerPreload ().

Router.paths builds URLs from the route tree: Router.paths["users"].Invoke(2).href is "/users/2".

Hooks

HookReturns
useParams ()The route's parameters: ps["id"] is a string option
useNavigate ()navigate.Invoke "/path", navigate.Invoke("/path", NavigateOptions(replace = true)), navigate.Invoke -1
useLocation ()The reactive location: pathname, search, hash, query, state
useSearchParams ()The query string, and a setter that merges keys: setSearch.Invoke {| page = 2 |}
useIsRouting ()true while a navigation settles
useMatch (fun () -> "/users/:id")The match of a pattern against the location, or None
useRouteMatches ()The routes matching the location, outermost first
useLinkState (fun () -> "/users")active, current and pending for a link
useBeforeLeave listenerRuns before leaving the route; e.preventDefault () blocks the navigation
usePreloadRoute ()Preloads a route by hand

Data

query caches a fetching function under a key. Read it with Solid's own primitives: the router has no async wrapper of its own in Solid 2.

let getUser = query ((fun (id: string) -> fetchUser id), "user")

[<SolidComponent>]
let UserName (props: RouteSectionProps) =
    let user = createMemo (fun (_: User option) -> getUser.Invoke(props.``params``["id"].Value))
    Loading (fallback = p () { "Loading" }) { h1 () { user().name } }

getUser.key and getUser.keyFor id name its cache entries for revalidate. Pass several values as a tuple or a record.

liveQuery is a query over a stream: its function returns an async iterable that yields the current value, then each change. Every caller with the same argument shares one connection, and a memo holds the latest value. F# has no async generators, so a producer usually wraps a JavaScript stream, such as an EventSource:

let roomMessages: LiveQuery<string, string> = liveQuery (subscribeToRoom, "messages")

[<SolidComponent>]
let Room (props: RouteSectionProps) =
    let latest = createMemo (fun (_: string option) -> roomMessages.Invoke(props.``params``["room"].Value))
    Loading (fallback = p () { "Connecting" }) { p () { latest () } }

roomMessages.status room reads the connection state (LiveQueryStatus) reactively, and revalidate with its key reconnects it.

action is a mutation with a URL. A form posts its FormData to it, and useAction calls it directly. Settled submissions are listed by useSubmissions. In-flight and optimistic state belongs to Solid's optimistic primitives, fed by the action's onSubmit hook.

let saveTitle =
    action ((fun (id: int) (data: obj) -> save id (unbox<string> (data?get "title"))), "save-title")

[<SolidComponent>]
let Editor () =
    form (action = (saveTitle.with' 3).url, method = Attr.Method.Post) {
        input (name = "title")
        button (type' = Attr.ButtonType.Submit) { "Save" }
    }

with' binds an action's first argument, in place of a hidden form field. An action that is not a server function needs a stable name, the second argument of action.

Not bound

  • Deferred until Partas.Solid has a server-functions story: server rendering helpers (@solidjs/router/server) and server component routes. They consume "use server" functions, which nothing in Partas.Solid produces yet.
  • Deferred until there is a design for F# route files: file-system routes (@solidjs/router/fs).
  • Not bound yet: the query cache helpers (get, set, clear), lazy route subtrees and RouterContext. Until then, bind one yourself with [<Import>].
  • Not needed: defineRoute and defineRoutes only add TypeScript types.

The 2.x router bindings, for Solid 1.9, are documented on the 2.x site.

Edit this page