UPDATE (8/15): you can now add additional routes as part of your code splitting strategy. See this issue comment for more info.
UPDATE (9/5): you can now block user navigation via the
confirmLeave
route option (and customize its appearance via thedisplayConfirmLeave
option). See this issue comment for more info. Doyarn upgrade redux-first-router@next
to get it.UPDATE (9/6): MUCH ANTICIPATED FEATURE: you can now do all sorts of optional params in your paths, match multiple similar paths to the same type, use regexes, and use params that capture multiple segments. See this issue comment for more info. Do
yarn upgrade redux-first-router@next
to get it.UPDATE (9/9): 4 new things: 1) route paths are now cached for better performance (less cycles wasted on parsing). 2) you can now do pathless routes!. I.e. you can use routes just for the purpose of the thunk feature, thereby achieving more uniformity across your async actions--just use RFR! Just dispatch the action type for which you have a thunk in your
routesMap
and it will be called like any other. To learn more about it: check this example. 3) now all thunks and callbacks (onBeforeChange
,onAfterChange
, etc) receive a 3rd "bag" argument which currently has a key for the currentaction
and anextra
key. More keys may be added later. Theextra
key is an option you provide toconnectRoutes
which is the equivalent of Redux-Thunk's "extra argument". It just happens to be in our bag. That said,onBeforeChange
used to receive just anaction
as a 3rd argument. So unfortunately this is a breaking change. Check the aforementioned example to learn more about this too. 4) lastly, now RFR skips actions with anerror
key; this should resolve a few quirks for developers using middleware that explicitly handle errors already. If it ever makes sense for RFR/Rudy to handle errors, we'll provide a robust solution. Half-assing as we've done doesn't make sense for now.UPDATE (9/16): There is now a
basename
option. Also,history
is no longer passed toconnectRoutes
. Thehistory
package is handled internally. Yup, a breaking change, but a super quick fix in one place. Check this comment for more info. Lastly, to upgrade now do this:yarn upgrade redux-first-router@rudy redux-first-router-link@rudy
:) ...next and master branches are locked in forever basically and will never be touched again :). Therudy
branch will become Rudy.UPDATE (9/18): There is now a
createHistory
option. Get it from therudy
tag on NPM. This was specifically implemented for people that want to use the hashHistory which I wasn't aware anyone wanted to do, plus it's also useful when testing your own implementation or forks.UPDATE (9/22): BREAKING CHANGE: now
toPath
andfromPath
on your routes are passed all path segments, even if they are numbers. No automatic transformations will happen if you provide these transformation functions. Get it @rudy on NPM.
The goal of Redux-First Router is to think of your app in states, not routes, not components, while keeping the address bar in sync. Everything is state, not components. Connect your components and just dispatch flux standard actions.
Articles You Should Read:
The thinking behind this package has been: "if we were to dream up a 'Redux-first' approach to routing from the ground up, what would it look like?" The result has been what we hope you feel to be one of those "inversion of control" scenarios that makes a challenging problem simple when coming at it from a different angle. We hope Redux-First Router comes off as an obvious solution.
Before we get started, there is some prior art, and you should check them out. Redux-First Router isn't the first stab at something like this, but--aside from this path being pre-validated--we feel it is the most complete, tested and spot on solution. We have reviewed what came before, stripped what was unnecessary, added what was needed, and generally focused on getting the developer experience right. Ultimately it offers far more than previous solutions. The best part is that once you set it up there's virtually nothing left to do. It's truly "set it and forget it." Let's get started.
And did we mention: it has first class support for React Navigation!
Install redux-first-router
and its peer dependency history
plus our small <Link />
package:
yarn add history redux-first-router redux-first-router-link
Full-Featured Universal Demo App (includes SSR + Splitting!):
https://github.com/faceyspacey/redux-first-router-demo
To automate routing. To be able to use Redux as is while keeping the URL in the address bar in sync. To think solely in terms of "state" and NOT routes, paths, route matching components. And of course for server side rendering to require no more than dispatching on the store like normal. Path params are just action payloads, and action types demarcate a certain kind of path. That is what routing in Redux is meant to be.
In practice, what that means is having the address bar update in response to actions and bi-directionally having actions dispatched in response to address bar changes, such as via the browser back/forward buttons. The "bi-directional" aspect is embodied in the diagram above where the first blue arrows points both ways--i.e. dispatching actions changes the address bar, and changes to the address bar dispatches actions.
In addition, here are some key obstacles Redux-First Router seeks to avoid:
react-router
and next.js
shouldComponentUpdate
are a must; routing frameworks
get in the way of optimizing animations.It's set-and-forget-it, so here's the most work you'll ever do! :+1:
import { connectRoutes } from 'redux-first-router'
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
import createHistory from 'history/createBrowserHistory'
import userIdReducer from './reducers/userIdReducer'
const history = createHistory()
// THE WORK:
const routesMap = {
HOME: '/home', // action <-> url path
USER: '/user/:id', // :id is a dynamic segment
}
const { reducer, middleware, enhancer } = connectRoutes(history, routesMap) // yes, 3 redux aspects
// and you already know how the story ends:
const rootReducer = combineReducers({ location: reducer, userId: userIdReducer })
const middlewares = applyMiddleware(middleware)
// note the order: enhancer, then middlewares
const store = createStore(rootReducer, compose(enhancer, middlewares))
import { NOT_FOUND } from 'redux-first-router'
export const userIdReducer = (state = null, action = {}) => {
switch(action.type) {
case 'HOME':
case NOT_FOUND:
return null
case 'USER':
return action.payload.id
default:
return state
}
}
And here's how you'd embed SEO/Redux-friendly links in your app, while making use of the triggered state:
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider, connect } from 'react-redux'
import Link from 'redux-first-router-link'
import store from './configureStore'
const App = ({ userId, onClick }) =>
<div>
{!userId
? <div>
<h1>HOME</h1>
// all 3 "links" dispatch actions:
<Link to="/user/123">User 123</Link> // action updates location state + changes address bar
<Link to={{ type: 'USER', payload: { id: 456 } }}>User 456</Link> // so does this
<span onClick={onClick}>User 5</span> // so does this, but without SEO benefits
</div>
: <h1>USER: {userId}</h1> // press the browser BACK button to go HOME :)
}
</div>
const mapStateToProps = ({ userId }) => ({ userId })
const mapDispatchToProps = (dispatch) => ({
onClick: () => dispatch({ type: 'USER', payload: { id: 5 } })
})
const AppContainer = connect(mapStateToProps, mapDispatchToProps)(App)
ReactDOM.render(
<Provider store={store}>
<AppContainer />
</Provider>,
document.getElementById('react-root')
)
note: ALL THREE clickable elements/links above will change the address bar while dispatching the corresponding USER
action. The only difference
is the last one won't get the benefits of SEO--i.e. an <a>
tag with a matching to
path won't be embedded in the page. What this means is
you can take an existing Redux app that dispatches similar actions and get the benefit of syncing your address bar without changing your code! The workflow we recommend is to
first do that and then, once you're comfortable, to use our <Link />
component to indicate your intentions to Google. Lastly, we recommend
using actions
as your to
prop since it doesn't marry you to a given URL structure--you can always change it in one place later (the routesMap
object)!
Based on the above routesMap
the following actions will be dispatched when the
corresponding URL is visited, and conversely those URLs will appear in the address bar
when actions with the matching type
and parameters are provided
as keys in the payload object:
URL | <-> | ACTION |
---|---|---|
/home | <-> | { type: 'HOME' } |
/user/123 | <-> | { type: 'USER', payload: { id: 123 } } |
/user/456 | <-> | { type: 'USER', payload: { id: 456 } } |
/user/5 | <-> | { type: 'USER', payload: { id: 5 } } |
note: if you have more keys in your payload that is fine--so long as you have the minimum required keys to populate the path
Lastly, we haven't mentioned redux-first-router-link
yet--Redux-First Router is purposely built in
a very modular way, which is why the <Link />
component is in a separate package. It's extremely simple
and you're free to make your own. Basically it passes the to
path on to Redux-First Router and calls
event.preventDefault()
to stop page reloads. It also can take an action object as a prop, which it will transform
into a URL for you! Its props API mirrors React Router's. The package is obvious enough once you get the hang of what's going on here--check it
out when you're ready: redux-first-router-link. And if
you're wondering, yes there is a NavLink
component with props like activeClass
and activeStyle
just like in React Router.
The routesMap
object allows you to match action types to express style dynamic paths, with a few frills.
Here's the primary (and very minimal easy to remember) set of configuration options available to you:
const routesMap = {
HOME: '/home', // plain path strings or route objects can be used
CATEGORY: { path: '/category/:cat', capitalizedWords: true },
USER: {
path: '/user/:cat/:name',
fromPath: path => capitalizeWords(path.replace(/-/g, ' ')),
toPath: value => value.toLowerCase().replace(/ /g, '-'),
},
}
note: the signature of fromPath
and toPath
offers a little more, e.g: (pathSegment, key) => value
. Visit routesMap docs for a bit more info when the time comes.
URL | <-> | ACTION |
---|---|---|
/home | <-> | { type: 'HOME' } |
/category/java-script | <-> | { type: 'CATEGORY', payload: { cat: 'Java Script' } } |
/user/elm/evan-czaplicki | <-> | { type: 'USER', payload: { cat: 'ELM', name: 'Evan Czaplicki' } } |
We left out one final configuration key available to you: a thunk. After the dispatch of a matching action, a thunk (if provided) will be called, allowing you to extract path parameters from the location reducer state and make asyncronous requests to get needed data:
const userThunk = async (dispatch, getState) => {
const { slug } = getState().location.payload
const data = await fetch(`/api/user/${slug}`)
const user = await data.json()
const action = { type: 'USER_FOUND', payload: { user } }
dispatch(action)
}
const routesMap = {
USER: { path: '/user/:slug', thunk: userThunk },
}
your
thunk
should return a promise for SSR to be able toawait
for its resolution and forupdateScroll()
to be called if using our scroll restoration package.
note: visit the location reducer docs to see the location
state's shape
URL | <-> | ACTION |
---|---|---|
/user/steve-jobs | <-> | { type: 'USER', payload: { slug: 'steve-jobs' } } |
n/a | n/a | { type: 'USER_FOUND', payload: { user: { name: 'Steve Jobs', slug: 'steve-jobs' } } } |
That's all folks! :+1:
options
parameter you should check out)meta
key is how our system communicates & how our action maintains its status as an "FSA")<Link prefetch />
powered by: react-universal-component + webpack-flush-chunks
What about if the URL is not found?
If the path is not found, or if actions fail to be converted to paths, our
NOT_FOUND
action type will be dispatched. You can apply it as a case in your reducer's switch statements. Here's where you get it:import { NOT_FOUND } from 'redux-first-router'
. We have strong idiomatic way to deal with it in server side rendering--check it out: server side rendering.
What if I don't want to use the thunk feature, can I use other ways of requesting the data?
Of course. This work along side any middleware of your choosing, and even GraphQL solutions like Apollo. But for the 80% use-case, "follow-up" thunks attached to routes gets a lot of bang for your buck via the context associated with the initial route action.
Ok, but what if I request my data in componentDidMount
?
This works great for that, but it's a naive strategy without a server-side recursive promise resolution service like Apollo offers. The problem with
componentDidMount
is that you can't generate all the state required to render your app without first rendering your app at least once. That means additional work on your part as well as cycles on the server (the latter of which is also a caveat for Apollo). It's also makes Redux's highly useful time-traveling tools unreliable. If that's where you're at in how you get things done, that's fine--but we recommend leveling up to a "dispatch to get state" strategy (rather than a "get state on render" approach), as that will provide way more predictability, which is especially useful when it comes to testing. When it comes to server side rendering there is no better option. We recommend looking at our server side rendering doc to see the recommended approach.
The middleware dispatches thunks asyncronously with no way for me to await them, how can I wait for asyncronously received data on the server?
Please visit the server side rendering doc. In short, thunks are not dispatched from the middleware on the server, but
connectRoutes
, returns athunk
in addition tomiddleware
,enhancer
andreducer
, which you can await on, and it will retreive any data corresponding to the current route! We think our solution is slick and sensible.
The server has no window
or history
, how can I get that on the server?
The history package provides a
createMemoryHistory()
function just for this scenario. It essentially generates a fakehistory
object based on therequest.path
express (or similar packages) will give you. It's painless. Check it out!
Does this work with React Native?
Yes, just like server side rendering, you can use the
history
package'screateMemoryHistory()
function. It's perfect for React Native'sLinking
API and push notifications in general. In fact, if you built your React Native app already and are just starting to deal with deep-linking and push notifications, Redux-First Router is perfectly suited to be tacked on in final stages with very few changes. We also have first-class support for React Navigation, which really is the crown jewel here and where we do most our work these days. It does some amazing things. Check it out!
Ok, but there's gotta be a catch--what changes should I expect to make if I start using Redux-First Router?
Primarily it will force you to consolidate the actions you use in your reducers. Whereas before you might have had several actions to trigger the same state, you will now centralize on a smaller number of actions that each correspond to a specific URL path. Your actions will become more "page-like", i.e. geared towards triggering page/URL transitions. That said, you absolutely don't need to have a URL for every action. In our apps, we don't. Just the actions that lead to the biggest visual changes in the page that we want search engines to pick up.
And what about actually getting links on the page for search engines to see?
Use redux-first-router-link. This package has been built in a modular way, which is why that's not in here. redux-first-router-link's
<Link />
and<NavLink />
components mirror React Router's. You should be using these on web to get<a>
tags on your page for Google. In React Native, just dispatch actions inonPress
handlers.
Why no route matching components like React Router?
Because they are unnecessary when the combination of actions and reducers lead to both a wider variety and better defined set of states, not to mention more singular. By "singular" we mean that you don't have to think in terms of both redux state AND address bar paths. You just think in terms of state after you setup your routes map. It makes your life simpler. It makes your code cleaner and easier to understand. It gives you the best control React + Redux has to offer when it comes to optimizing rendering for animations.
What about all the code splitting features Next.js has to offer?
They certainly crush it when it comes to code splitting. There's no doubt about it. But check out their Redux example where it seems to have a different
store
per page. I've asked, and they do merge, but it complicates how you will use Redux. If your app is very page-like, great--but we think the whole purpose of tools like React and Redux is to build "apps" not pages. The hallmark of an app is seamless animated transitions where you forget you're on a specific page. You need full control of rendering to do that at the highest level.shouldComponentUpdate
, pure functions and reselect become your best friends. Everything else gets in the way. And of course Redux-First Router stays out of the way. Straightup, let us know if you think we nailed it or what we're missing. Feel free to use github issues.
Gee, I've never seen a Redux middleware/enhancer tool return so many things to use for configuring the store???
Part of what Redux-First Router does so well (and one of its considerations from the start) is server side rendering. All these aspects depend on state unique to each visit/request. The returned
middleware
,enhancer
,reducer
andthunk
functions share enclosed state (i.e. within a "closure") in a per instance fashion. Most of the code is written as pure utility functions and we are very proud about that. But what's not is returned to you in a way that will insure state is not shared between requests on the server. In short, we have spared no expense to get this package as tight as possible. Watch the video below to get an idea of how the system works and its overall simplicity:
video coming soon...
We use commitizen, so run npm run cm
to make commits. A command-line form will appear, requiring you answer a few questions to automatically produce a nicely formatted commit. Releases, semantic version numbers, tags, changelogs and publishing to NPM will automatically be handled based on these commits thanks to semantic-release. Be good.
Reviewing a package's tests are a great way to get familiar with it. It's direct insight into the capabilities of the given package (if the tests are thorough). What's even better is a screenshot of the tests neatly organized and grouped (you know the whole "a picture says a thousand words" thing).
Below is a screenshot of this module's tests running in Wallaby ("An Integrated Continuous Testing Tool for JavaScript") which everyone in the React community should be using. It's fantastic and has taken my entire workflow to the next level. It re-runs your tests on every change along with comprehensive logging, bi-directional linking to your IDE, in-line code coverage indicators, and even snapshot comparisons + updates for Jest! I requestsed that feature by the way :). It's basically a substitute for live-coding that inspires you to test along your journey.
Universal
product line. Make sure to check out!