State first routing
With Overmind you can use whatever routing solution your selected view layer provides. This will most likely intertwine routing state with your component state, which is something Overmind would discourage, but you know… whatever you feel productive in, you should use :-) In this guide we will look into how you can separate your router from your components and make it part of your application state instead. This is more in the spirit of Overmind and throughout this guide you will find benefits of doing it this way.
We are going to use PAGE.JS as the router and we will look at a complex routing example where we open a page with a link to a list of users. When you click on a user in the list we will show that user in a modal with the URL updating to the id of the user. In addition we will present a query parameter that reflects the current tab inside the modal.
We will start with a simple naïve approach and then tweak our approach a little bit for the optimal solution.
Before we go into the router we want to set up the application. We have some state helping us express the UI explained above. In addition we have three actions.
- 1.showHomePage tells our application to set the current page to home
- 2.showUsersPage tells our application to set the current page to users and fetches the users as well
- 3.showUserModal tells our application to show the modal by setting an id of a user passed to the action. This action will also handle the switching of tabs later.
overmind/actions.js
export const showHomePage = ({ state }) => {
state.currentPage = 'home'
}
export const showUsersPage = async ({ state, effects }) => {
state.modalUser = null
state.currentPage = 'users'
state.isLoadingUsers = true
state.users = await effects.api.getUsers()
state.isLoadingUsers = false
}
export const showUserModal = async ({ state, effects }, params) => {
state.isLoadingUserDetails = true
state.modalUser = await effects.api.getUserWithDetails(params.id)
state.isLoadingUserDetails = false
}
Page.js is pretty straightforward. We basically want to map a URL to trigger an action. To get started, let us first add Page.js as an effect and take the opportunity to create a custom API. When a URL triggers we want to pass the params of the route to the action linked to the route:
overmind/effects.js
import page from 'page'
export const router = {
initialize(routes) {
Object.keys(routes).forEach(url => {
page(url, ({ params }) => routes[url](params))
})
page.start()
},
open: (url) => page.show(url)
}
Now we can use Overmind’s onInitialize to configure the router. That way the initial URL triggers before the UI renders and we get to set our initial state.
overmind/actions.js
// The other actions
export const onInitializeOvermind = ({ actions, effects }) => {
effects.router.initialize({
'/': actions.showHomePage,
'/users': actions.showUsersPage,
'/users/:id': actions.showUserModal
})
}
Take notice here that we are actually passing in the params from the router, meaning that the id of the user will be passed to the action.
When we now go to the list of users the list loads up and is displayed. When we click on a user the URL changes, our showUser action runs and indeed, we see a user modal.
React
Angular
Vue
// components/App.jsx
import * as React from 'react'
import { useAppState } from '../overmind'
import Users from './Users'
const App = () => {
const state = useAppState()
return (
<div className="container">
<nav>
<a href="/">Home</a>
<a href="/users">Users</a>
</nav>
{state.currentPage === 'home' ? <h1>Hello world!</h1> : null}
{state.currentPage === 'users' ? <Users /> : null}
</div>
)
}
export default App
// components/Users.jsx
import * as React from 'react'
import { useAppState } from '../overmind'
import UserModal from './UserModal'
const Users = () => {
const state = useAppState()
return (
<div className="content">
{state.isLoadingUsers ? (
<h4>Loading users...</h4>
) : (
<ul>
{state.users.map(user => (
<li key={user.id}>
<a href={"/users/" + user.id}>{user.name}</a>
</li>
))}
</ul>
)}
{state.isLoadingUserDetails || state.modalUser ? <UserModal /> : null}
</div>
)
}
export default Users
// components/UserModal.jsx
import * as React from 'react'
import { useAppState } from '../overmind'
const UserModal = () => {
const state = useAppState()
return (
<a href="/users" className="backdrop">
<div className="modal">
{state.isLoadingUserDetails ? (
<h4>Loading user details...</h4>
) : (
<>
<h4>{state.modalUser.name}</h4>
<h6>{state.modalUser.details.email}</h6>
<nav>
<a href={"/users/" + state.modalUser.id + "?tab=0"}>bio</a>
<a href={"/users/" + state.modalUser.id + "?tab=1"}>address</a>
</nav>
{state.currentUserModalTabIndex === 0 ? (
<div className="tab-content">{state.modalUser.details.bio}</div>
) : null}
{state.currentUserModalTabIndex === 1 ? (
<div className="tab-content">{state.modalUser.details.address}</div>
) : null}
</>
)}
</div>
</a>
)
}
export default UserModal
// components/app.component.ts
import { Component } from '@angular/core';
import { Store } from '../overmind'
@Component({
selector: 'app-component',
template: `
<div class="container" *track>
<nav>
<a href="/">Home</a>
<a href="/users">Users</a>
</nav>
<h1 *ngIf="state.currentPage === 'home'">Hello world!</h1>
<users-list *ngIf="state.currentPage === 'users'"></users-list>
</div>
`
})
export class AppComponent {
state = this.store.select()
constructor(private store: Store) {}
}
// components/users-list.component.ts
import { Component } from '@angular/core';
import { Store } from '../overmind'
@Component({
selector: 'users-list',
template: `
<div class="content" *track>
<h4 *ngIf="state.isLoadingUsers">Loading users...</h4>
<ul *ngIf="!state.isLoadingUsers">
<li *ngFor="let user of state.users;trackby: trackById">
<a href={"/users/" + user.id}>{{user.name}}</a>
</li>
</ul>
<user-modal *ngIf="state.isLoadingUserDetails || state.userModal"></user-modal>
</div>
`
})
export class UsersList {
state = this.store.select()
constructor(private store: Store) {}
trackById(index, user) {
return user.id
}
}
// components/user-modal.component.ts
import { Component } from '@angular/core';
import { Store } from '../overmind'
@Component({
selector: 'user-modal',
template: `
<a href="/users" class="backdrop">
<div class="modal">
<h4 *ngIf="state.isLoadingUserDetails">Loading user details...</h4>
<div *ngIf="!state.isLoadingUserDetails">
<h4>{{state.modalUser.name}}</h4>
<h6>{{state.modalUser.details.email}}</h6>
<nav>
<a [href]="'/users/' + state.modalUser.id + '?tab=0'">bio</a>
<a [href]="'/users/' + state.modalUser.id + '?tab=1'">address</a>
</nav>
<div
*ngIf="state.currentUserModalTabIndex === 0"
class="tab-content"
>
{{modalUser.details.bio}}
</div>
<div
*ngIf="state.currentUserModalTabIndex === 1"
class="tab-content"
>
{{modalUser.details.address}}
</div>
</div>
</div>
</a>
`
})
export class UserModal {
state = this.store.select()
constructor(private store: Store) {}
}
// components/App.vue
<template>
<div class="container">
<nav>
<a href="/">Home</a>
<a href="/users">Users</a>
</nav>
<h1 v-if="state.currentPage === 'home'">Hello world!</h1>
<users-list v-if="state.currentPage === 'users'"></users-list>
</div>
</template>