{"id":18552423,"url":"https://github.com/andrejewski/raj-spa","last_synced_at":"2025-10-19T15:05:37.957Z","repository":{"id":57149049,"uuid":"99516161","full_name":"andrejewski/raj-spa","owner":"andrejewski","description":"Single Page Applications for Raj","archived":false,"fork":false,"pushed_at":"2020-05-11T17:29:22.000Z","size":209,"stargazers_count":4,"open_issues_count":5,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-06-16T07:05:35.625Z","etag":null,"topics":["code-splitting","single-page-app"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andrejewski.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-08-06T22:12:25.000Z","updated_at":"2019-05-17T13:00:00.000Z","dependencies_parsed_at":"2022-09-06T13:01:53.260Z","dependency_job_id":null,"html_url":"https://github.com/andrejewski/raj-spa","commit_stats":null,"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/andrejewski/raj-spa","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrejewski%2Fraj-spa","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrejewski%2Fraj-spa/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrejewski%2Fraj-spa/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrejewski%2Fraj-spa/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andrejewski","download_url":"https://codeload.github.com/andrejewski/raj-spa/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrejewski%2Fraj-spa/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261162065,"owners_count":23118221,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-splitting","single-page-app"],"created_at":"2024-11-06T21:14:10.516Z","updated_at":"2025-10-19T15:05:37.878Z","avatar_url":"https://github.com/andrejewski.png","language":"JavaScript","readme":"# Raj SPA\n\u003e Single Page Apps for [Raj](https://github.com/andrejewski/raj)\n\n```sh\nnpm install raj-spa\n```\n\n[![npm](https://img.shields.io/npm/v/raj-spa.svg)](https://www.npmjs.com/package/raj-spa)\n[![Build Status](https://travis-ci.org/andrejewski/raj-spa.svg?branch=master)](https://travis-ci.org/andrejewski/raj-spa)\n[![Greenkeeper badge](https://badges.greenkeeper.io/andrejewski/raj-spa.svg)](https://greenkeeper.io/)\n\n## Usage\n\n```js\nimport spa from 'raj-spa'\nimport {program} from 'raj-react'\nimport React from 'react'\n\nimport router from './router'\nimport homepage from './pages/home'\n\nfunction getRouteProgram (route) {\n  if (route === '/') {\n    // Static program routing\n    return homepage\n  }\n\n  if (route.startsWith('/users/')) {\n    // Dynamic, code-split routing (using ES6 import())\n    // i.e. you can return a promise which resolves a program\n    const userId = route.split('/').pop()\n    return System.import('./pages/user.js').then(page =\u003e page.default(userId))\n  }\n\n  // 404\n  return System.import('./pages/not-found.js')\n}\n\nconst initialProgram = {\n  init: [],\n  update: () =\u003e [],\n  view () {\n    return \u003cp\u003eLoading application...\u003c/p\u003e\n  }\n}\n\nexport default program(React.Component, () =\u003e spa({\n  router,\n  getRouteProgram,\n  initialProgram\n}))\n```\n\n## Documentation\n\nThe `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.\n\n#### Required configuration\n\n| Property | Type | Description |\n| -------- | ---- | ----------- |\n| `router` | `RajRouter` | The router to which the SPA will subscribe.\n| `getRouteProgram` | `route =\u003e RajProgram` | The mapping from routes to programs which receives a route and returns a `RajProgram` or a Promise that resolves a `RajProgram`.\n| `initialProgram` | `RajProgram` | The 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`.\n\n#### Optional configuration\n\n| Property | Type | Description |\n| -------- | ---- | ----------- |\n| `getErrorProgram` | `Error =\u003e RajProgram` | The program used when loading a program rejects with an error. `getErrorProgram` receives the error and returns a `RajProgram`.\n| `containerView` | function | A container view which wraps the entire application. The function will receive a `ContainerViewModel` and the sub program's `view` result to encapsulate.\n\n#### Types\n\n##### `RajProgram`\nThis is a normal Raj program.\n\n```ts\ninterface RajProgram {\n  init: [model, effect];\n  update: function(msg, state);\n  view: function(state, dispatch);\n}\n```\n\n##### `RajRouter`\nRaj 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.\n\n```ts\ninterface RajRouter {\n  subscribe (): {\n    effect: (dispatch: (message: any) =\u003e void) =\u003e void,\n    cancel: () =\u003e void\n  };\n}\n```\n\nThe `effect` method is an effect so it will receive `dispatch` which it will call with `dispatch(route)` at the appropriate times.\n\nThe `cancel` method cancels the subscription to route changes. When the `cancel` function calls, route changes should stop dispatching.\n\nNote: `route` can be anything that your `getRouteProgram` understands.\n\n##### `ContainerViewModel`\nThe `containerView` function receives this object.\n\n```ts\ninterface ContainerViewModel {\n  isTransitioning: boolean;\n}\n```\n\n### Keyed programs and nested SPAs\nBy default, every emitted route causes the teardown of the current program and the set up of the next program.\nFor 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.\nTo preserve a program between route changes, we wrap that program in `keyed`.\n\n```js\nfunction getRouteProgram (route, { keyed }) {\n  if (route === '/simple-page') {\n    return simpleProgram\n  }\n  if (route.startsWith('/nested-spa')) {\n    return keyed('my-key', router =\u003e nestedSpa(router))\n  }\n}\n```\n\nThe `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.\n\n### Questions\n\n#### Does this work with React Native?\nYes, Raj SPA is unaware of any particular URL-based routing. You can define a \"headless\" router that emits routes you pick.\n\n#### Server-side rendering?\nIf you are using React, the `raj-react` bindings return a React Component that works with [`ReactDOMServer`](https://facebook.github.io/react/docs/react-dom-server.html) to get started.\n\nMore advanced/experimental strategies exist to increase performance and content delivery, but I won't give advice until I try them out myself.\n\n#### Are there any routers I can steal?\nHere is a hash-change router implementation.\n\n```js\nexport default {\n  subscribe () {\n    let listener\n    return {\n      effect (dispatch) {\n        listener = () =\u003e dispatch(window.location.hash)\n        window.addEventListener('hashchange', listener)\n        listener() // dispatch initial route\n      },\n      cancel () {\n        window.removeEventListener('hashchange', listener)\n      }\n    }\n  }\n}\n```\n\nI recommend using something on top of this like [`tagmeme`](https://github.com/andrejewski/tagmeme) to define and then pattern match on different pages.\n\n#### My pages have the same header/footer...?\nUse `containerView` to describe all the universal parts of your app.\n\n```js\nexport function containerView (containerViewModel, subView) {\n  return \u003cdiv\u003e\n    \u003cheader\u003e...\u003c/header\u003e\n    \u003cmain\u003e{subView}\u003c/main\u003e\n    \u003cfooter\u003e...\u003c/footer\u003e\n  \u003c/div\u003e\n}\n```\n\nOr, you can nest the SPA component in a surrounding program/component.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrejewski%2Fraj-spa","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandrejewski%2Fraj-spa","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrejewski%2Fraj-spa/lists"}