Home

Awesome

Raj SPA

Single Page Apps for Raj

npm install raj-spa

npm Build Status Greenkeeper badge

Usage

import spa from 'raj-spa'
import {program} from 'raj-react'
import React from 'react'

import router from './router'
import homepage from './pages/home'

function getRouteProgram (route) {
  if (route === '/') {
    // Static program routing
    return homepage
  }

  if (route.startsWith('/users/')) {
    // Dynamic, code-split routing (using ES6 import())
    // i.e. you can return a promise which resolves a program
    const userId = route.split('/').pop()
    return System.import('./pages/user.js').then(page => page.default(userId))
  }

  // 404
  return System.import('./pages/not-found.js')
}

const initialProgram = {
  init: [],
  update: () => [],
  view () {
    return <p>Loading application...</p>
  }
}

export default program(React.Component, () => spa({
  router,
  getRouteProgram,
  initialProgram
}))

Documentation

The raj-spa package exports a single function which takes the following arguments and returns a RajProgram configuration which can plug into raj-react or another Raj program.

Required configuration

PropertyTypeDescription
routerRajRouterThe router to which the SPA will subscribe.
getRouteProgramroute => RajProgramThe mapping from routes to programs which receives a route and returns a RajProgram or a Promise that resolves a RajProgram.
initialProgramRajProgramThe initial program used before the first received route from the router resolves. The transition to the first route's program should be instantaneous if a static program returns from getRouteProgram.

Optional configuration

PropertyTypeDescription
getErrorProgramError => RajProgramThe program used when loading a program rejects with an error. getErrorProgram receives the error and returns a RajProgram.
containerViewfunctionA container view which wraps the entire application. The function will receive a ContainerViewModel and the sub program's view result to encapsulate.

Types

RajProgram

This is a normal Raj program.

interface RajProgram {
  init: [model, effect];
  update: function(msg, state);
  view: function(state, dispatch);
}
RajRouter

Raj SPA will work with any router that is compatible with the following interface. Note that Raj Spa makes no assumption about the underlying navigation rules. Choices, such as push-state or hash-state, are all encapsulated by the router.

interface RajRouter {
  subscribe (): {
    effect: (dispatch: (message: any) => void) => void,
    cancel: () => void
  };
}

The effect method is an effect so it will receive dispatch which it will call with dispatch(route) at the appropriate times.

The cancel method cancels the subscription to route changes. When the cancel function calls, route changes should stop dispatching.

Note: route can be anything that your getRouteProgram understands.

ContainerViewModel

The containerView function receives this object.

interface ContainerViewModel {
  isTransitioning: boolean;
}

Keyed programs and nested SPAs

By default, every emitted route causes the teardown of the current program and the set up of the next program. For a minor change in query params or a major change in pages that share programs(s), this can lose useful state and thrash the view. To preserve a program between route changes, we wrap that program in keyed.

function getRouteProgram (route, { keyed }) {
  if (route === '/simple-page') {
    return simpleProgram
  }
  if (route.startsWith('/nested-spa')) {
    return keyed('my-key', router => nestedSpa(router))
  }
}

The keyed function takes a key and function to call to get the program. The key can be any truthy value, which will be compared with the previous key (or lack thereof) using === strict equality. If the key is the same between getRouteProgram calls, the program is preserved. In order to respond to route changes within a keyed program, the keyed function is passed a RajRouter which emits routes and can be subscribed to by the program.

Questions

Does this work with React Native?

Yes, Raj SPA is unaware of any particular URL-based routing. You can define a "headless" router that emits routes you pick.

Server-side rendering?

If you are using React, the raj-react bindings return a React Component that works with ReactDOMServer to get started.

More advanced/experimental strategies exist to increase performance and content delivery, but I won't give advice until I try them out myself.

Are there any routers I can steal?

Here is a hash-change router implementation.

export default {
  subscribe () {
    let listener
    return {
      effect (dispatch) {
        listener = () => dispatch(window.location.hash)
        window.addEventListener('hashchange', listener)
        listener() // dispatch initial route
      },
      cancel () {
        window.removeEventListener('hashchange', listener)
      }
    }
  }
}

I recommend using something on top of this like tagmeme to define and then pattern match on different pages.

My pages have the same header/footer...?

Use containerView to describe all the universal parts of your app.

export function containerView (containerViewModel, subView) {
  return <div>
    <header>...</header>
    <main>{subView}</main>
    <footer>...</footer>
  </div>
}

Or, you can nest the SPA component in a surrounding program/component.