naglfar

    2.3.0 • Public • Published

    Naglfar

    A router that keeps all state in redux. It treats the url like a form input: it fires actions on change which update the location in your state. This is accessible from any redux-connected component: no need for a separate RouteProvider.

    It doesn't need to own/wrap your application. It provides a Fragment pattern to partition your UI based on current route, including route status codes.

    It works both in the browser and on the server, and offers:

    • Server-side rendering
    • Data fetching prior to transition (via dispatching actions)
    • Status codes
    • Not-found routes (and any status-code based route)
    • Redirects
    • Prefetching data (pre-firing actions without modifying state) for Links to speed up navigation

    Requires the history module.

    How to use

    Install via npm i -S naglfar or yarn add naglfar.

    Create your routes. Example:

    // routes.js
    import {routeFragment, routeRedirect} from 'naglfar'
    import {
      getSubredditFeed,
      getStory
    } from './actionCreators'
    
    // Actions per route can be regular actionCreators, async thunks,
    // action objects, or action-type strings, or an array of any of those
    export const SubredditRoute = routeFragment('/r/:subreddit', getSubredditFeed)
    export const StoryRoute = routeFragment('/r/:subreddit/:story', [getSubredditFeed, getStory])
    export const NotFoundRoute = routeFragment(404)
    export const ErrorRoute = routeFragment(500)
    
    routeRedirect('/', '/r/front_page')

    Actions are passed the route params, and should return promises (this isn't necessary, but all promises return by your actions will be resolved before the state will transition to the specified route).

    Create your app:

    import {Link, Fragment} from 'naglfar'
    import {
      SubredditRoute,
      StoryRoute
    } from './routes'
    
    export default () => (
      <div>
        <nav>
          <Link to='/r/all'>All</Link>
          <Link to='/r/games' prefetchData>Games</Link>
          <Link to='/r/cars'>Cars</Link>
        </nav>
         // You can create route-based views directly as well
         // But the router doesn't know about these until after the first render,
         // so their routes can't be relied on for data-fetching in SSR
        <Fragment forRoute={'/r/front_page'}>
          <FrontPage />
        </Fragment>
        <SubredditRoute>
          <Feed />
        </SubredditRoute>
        <StoryRoute>
          <Story />
        </StoryRoute>
      </div>
    )

    Render it

    import {createStore, applyMiddleware} from 'redux'
    import thunk from 'redux-thunk'
    import {Provider} from 'react-redux'
    import createHistory from 'history/createBrowserHistory'
    import router, {reducer: routeReducer} from 'naglfar'
    import App from './App'
    import appReducer from './store'
    
    const composeReducers = (first, ...fns) =>
      (s, a) => fns.reduce((s, fn) => fn(s, a), first(s, a))
    
    const configureStore = (history, initialState) =>
      createStore(
        composeReducers(appReducer, routeReducer),
        initialState,
        applyMiddleware(thunk, router(history))
      )
    
    const store = configureStore(createHistory(), {})
    
    export default () => {
      render(
        <Provider store={store}>
          <App />
        </Provider>
      )
    }

    Server-side render middleware (example using express)

    import React from 'react'
    import {renderToString, renderToStaticMarkup} from 'react-dom/server'
    import {Provider} from 'react-redux'
    import createHistory from 'history/createMemoryHistory'
    import {resolveLocation} from 'naglfar'
    import Root from './Root'
    import App from './App'
    
    const renderHtml = ({store}) => {
      const body = renderToString(
        <Provider store={store}>
          <App />
        </Provider>
      )
    
      const rootMarkup = renderToStaticMarkup(
        <Root
          content={body}
          initialState={store.getState()}
        />
      )
    
      return `<!doctype html>\n${rootMarkup}`
    }
    
    const resolvePath = ({path, store}) => {
      resolveLocation(path, store.dispatch)
        .then(({status, url}) => {
          if (url) return {status, redirect: url}
          return {status, html: renderHtml({store})}
        })
    }
    
    const renderMiddleware = (req, res) => {
      const history = createHistory({
        initialEntries: [req.url]
      })
      resolvePath({path: req.url, store: configureStore(history)})
        .then(({redirect, status, html}) => {
          if (redirect) res.redirect(status, redirect)
          res.status(status).send(html)
        })
    }
    
    export default renderMiddleware
    
    // use like: server.get('*', renderMiddleware)

    Useful stuff

    If you need to access location data, such as query and route params, they are accessible on your redux store under the top level location key. Example:

    import {connect} from 'react-redux'
    import FilterOptions from '../FilterOptions'
    
    const mapStateToProps = ({location}) => ({
      sortOrder: location.query.sortOrder,
      page: location.params.page
    })
    
    export default connect(mapStateToProps)(FilterOptions)

    If you want to only handle valid routes, you can extract the whitelist of all registered routes:

    import {whitelist} from 'naglfar'
    
    const rejectInvalidRoutesMiddleware = (req, res, next) =>
      whitelist.includes(req.path) ? next() : res.status(404).send()

    Install

    npm i naglfar

    DownloadsWeekly Downloads

    17

    Version

    2.3.0

    License

    MIT

    Unpacked Size

    25.1 kB

    Total Files

    7

    Last publish

    Collaborators

    • benoneal