Frontend Navigation with Custom Actons

This doc covers how to handle client-side navigation in the application using nexus.

The application uses Reitit for routing in combination with our custom action/effects system for state management. This provides a powerful and flexible way to handle navigation in your Single Page Application (SPA).

There are main navigation actions available:

  • :router/navigate - An action that triggers navigation

Basic Navigation

Using :router/navigate

The most common way to trigger navigation is with the :router/navigate action:

{:on {:click [[:router/navigate {:to ::routes/dashboard}]]}}

This will navigate the user to the dashboard route defined in your routes configuration.

You can include path parameters and query parameters in your navigation:

{:on {:click [[:router/navigate
               {:to ::routes/user-profile
                :path-params {:user-id "123"}
                :query-params {:tab "settings"}}]]}}

This will navigate to a URL like /user/123?tab=settings (assuming your route is defined as something like /user/:user-id).

Handling Navigation Results

When a navigation occurs, the route information is stored in DataScript and can be queried:

;; Query current route
(ds/q '[:find (pull ?e [*])
        :where [?e :route/id :current-route]]
      @!conn)

;; Or more frequently by entity
(ds/entity db [:route/id :current-route])

Common Navigation Patterns

A common pattern is to navigate after an API call succeeds:

{:on {:submit [[:data/command {:command/kind :some/endpoint
                               :command/data form-data}
                {:on-success [[:router/navigate {:to ::routes/success-page}]]}]]}}

Conditional Navigation in Components

(defn login-form [ctx]
  (let [auth-result (get-in ctx [:state :auth :result])]
    [:form {:on {:submit [[:auth/login form-data]]}}
     ;; Form elements
     (when (:success auth-result)
       {:on {:mount [[:router/navigate {:route ::routes/dashboard}]]}})]))

Redirecting to Login

When an unauthorized action is attempted, redirect to the login page:

{:on {:error [[:router/navigate {:to ::routes/login}]]}}

There is also the [:saas.auth/maybe-init] action that checks if the user is logged in, fetches the auth information from the server and redirects to login if the user wasn't logged in.

Route Definition

Routes are defined in src/cljc/saas/routes.cljc using Reitit's routing syntax. Here's a simplified example:

(def routes
  [""
   ["/" {:name ::index
         :seo/title "Home"}]

   ["/dashboard" {:name ::dashboard
                  :seo/title "Dashboard"}]

   ["/user/:user-id" {:name ::user-profile
                      :seo/title "User Profile"
                      :view user/profile-page
                      :parameters {:path {:user-id string?}
                                   :query {:tab string?}}}]])

SEO Considerations

When navigating to a new route, the navigation action will automatically update the page title and meta description based on the :seo/title and :seo/description properties defined in your route data.

Route Redirects

The application supports automatic redirects through the :redirect-to route property. This allows you to define routes that automatically redirect to another route when accessed.

How It Works

  1. Define a route with the :redirect-to property in src/cljc/saas/routes.cljc:
["/settings"
 ["" {:name ::settings
      :router/redirect {:to ::profile})]
 ["/profile" ...]]
  1. When a user navigates to /settings, the router detects the :router/redirect property and automatically redirects to the specified route (::routes/profile-settings in this example).

Common Use Cases

  • Default pages for sections (e.g., redirecting /settings to /settings/profile)
  • Legacy URLs that should redirect to new locations
  • Creating logical groupings of routes with a default landing page

Implementation Details

The navigation system is implemented in:

  • src/cljs/saas/core.cljs: Sets up the router and initializes route handling
  • src/cljc/saas/actions.cljc: Defines the navigation action handlers
  • src/cljc/saas/routes.cljc: Defines the application routes
  • src/cljs/saas/router.cljs: Handles route matching and redirection logic

Routing by route-name

You can use saas.routes/href to obtain the physical href location. Example: (href :saas.routes/login) => "/auth/login". This is handy if you prefer to use keyword based routing troughout the app as it is easier to do refactorings like this.

Example Usage

Basic Navigation Button

(defn nav-button []
  [:button {:on {:click [[:router/navigate {:to ::routes/dashboard}]]}}
   "Go to Dashboard"])
(defn signup-form [ctx]
  (let [form-data (get-form-data ctx :signup)]
    [:form {:on {:submit [[:auth/register form-data]
                          [:router/navigate {:route ::routes/onboarding}]]}}
     ;; Form elements here
     ]))

Programmatic Navigation

You can also trigger navigation programmatically by dispatching actions:

;; In an action handler
(defn ^:action-handler handle-auth-success
  [ctx action]
  (match action
    [:auth/success user-data]
    {:transactions [{:user/id (:id user-data) :user/name (:name user-data)}]
     :effects [[:router/navigate {:route ::routes/dashboard}]]}

    :else nil))

This provides a clean, declarative way to handle navigation that integrates seamlessly with the rest of the application's action system.