Routedux — Routes the Redux Way
38 |
39 |
44 |
Routedux routes URLs to Redux actions and vice versa.
56 |57 | Your application doesn't need to know it lives in a browser, but 58 | your users want pretty urls and deep links. 59 |
60 |62 | Wait, my application doesn't need to know it lives in a browser? 63 |
64 |65 | URLs are great for finding things on the internet. But a single 66 | page application is not the same as a collection of resources that 67 | lives on a remote server. 68 |
69 |70 | A single page application is a web application only in the sense 71 | that it lives on the web. URLs are are not essential to it working 72 | well. 73 |
74 |75 | URLs give users accessing your application in a browser the 76 | ability to bookmark a particular view in your application so that 77 | their expectation of browser-based applications will continue to 78 | work. 79 |
80 |81 | We think that's a good thing, but we also don't think the idea of 82 | url paths should be littered through your application. 83 |
84 |85 | When you are developing a redux application, you want your UI to 86 | be a pure function of the current state tree. 87 |
88 |89 | By adding routes to that, it makes it harder to test. And this 90 | difficulty can be compounded by other decisions about how to add 91 | routes to your application. 92 |
93 |An alternative approach
96 |97 | React Router is the currently-accepted way to do URL routing in 98 | React applications. For a standard React application without 99 | Redux, this solution isn't too bad. But once you add Redux, things 100 | get difficult. 101 |
102 |
103 | We basically discovered the same lessons as Formidable
104 | Labs:
127 | Once you separate URLs from your application state, you can easily 128 | port it to other environments that don't know what URLs are, and 129 | by simply removing the routing declaration, things will work as 130 | before. 131 |
132 |133 | As an added (and we think absolutely essential) benefit, your 134 | entire application becomes easier to test, as rendering is a pure 135 | function of Redux state, and model logic and route actions are 136 | entirely encapsulated in Redux outside of the app. 137 |
138 |Demo Site
141 |142 | We have a demo codebase at 143 | demo repository. 146 |
147 |Simple Routing in 25 lines
150 |import installBrowserRouter from 'routedux';
151 | import {createStore, compose} from 'redux';
const LOAD_USER = 'LOAD_USER';
function currentUserId() {
152 | return 42;
153 | };
function reduce(state = initialState(), action) {
154 | ...
155 | }
const routesConfig = [
156 | ['/user/:id', LOAD_USER, {}],
157 | ['/user/me', LOAD_USER, {id: currentUserId()}],
158 | ['/article/:slug', 'LOAD_ARTICLE', {}],
159 | ['/', 'LOAD_ARTICLE', {slug: "home-content"}]
160 | ];
const {enhancer} = installBrowserRouter(routesConfig);
const store = createStore(reduce, compose(
161 | enhancer
162 | ));
163 | 164 | Any time a handled action fires the url in the address bar will 165 | change, and if the url in the address bar changes the 166 | corresponding action will fire (unless the action was initiated by 167 | a url change). 168 |
169 |Route matching precedence - which route matches best?
172 |173 | Route precedence is a function of the type of matching done in 174 | each segment and the order in which the wildcard segments match. 175 | Exact matches are always preferred to wildcards moving from left 176 | to right. 177 |
178 |const routesInOrderOfPrecedence = [
179 | ['/user/me/update', '/user/me'], // both perfectly specific - will match above any wildcard route
180 | '/user/me/:view',
181 | '/user/:id/update', // less specific because 'me' is exact match, while :id is a wildcard
182 | '/user/:id/:view'
183 | ];
184 | Fragment component
187 |188 | Given that every UI state will be in your state tree as a function 189 | of your reducer logic, you can express any restriction on which 190 | parts of the UI display, even those that have nothing to do with 191 | the specific transformations caused by your URL actions. 192 |
193 |const state = {
194 | menu: ...
195 | }
const view = (
196 | <PageFrame>
197 | <Fragment state={state} filterOn="menu">
198 | <Menu />
199 | </Fragment>
200 | </PageFrame>
201 | )
// If menu is truthy, this renders as:
202 | (
203 | <PageFrame>
204 | <Menu />
205 | </PageFrame>
206 | )
// If menu is falsy, this renders as:
207 | (
208 | <PageFrame>
209 | </PageFrame>
210 | )
// If property is missing in path, it's falsy.
211 | const view = (
212 | <PageFrame>
213 | <Fragment state={state} filterOn="menu.missingProp.something">
214 | <Menu />
215 | </Fragment>
216 | </PageFrame>
217 | )
// Renders as:
218 | (
219 | <PageFrame>
220 | </PageFrame>
221 | )
222 | ActionLink and pathForAction(action)
225 |226 | Occasionally it is nice to render URLs inside of your application. 227 |
228 |
229 | As a convenience, we have attached
230 | pathForAction
to the
231 | store
object, which uses the same
232 | matcher that the action matcher uses. This allows you to create
233 | links in your application by using the actions.
234 |
const routesConfig = [
236 | ['/user/:id', LOAD_USER, {}],
237 | ['/user/me', LOAD_USER, {id: currentUserId()}]
238 | ];
239 | /* ... do store initialization */
store.pathForAction({type:LOAD_USER, id: currentUserId()});
240 | /* returns /user/me */
/* ActionLink */
import { ActionLink as _ActionLink } from "routedux";
const store = createStore(...);
class ActionLink extends _ActionLink {
241 | constructor(props) {
242 | super({ ...props });
243 | this.store = store;
244 | }
245 | }
const action = {
246 | type: LOAD_USER,
247 | id: 123
248 | };
return (
249 | <ActionLink action={action}>Link Text</ActionLink>
250 | );
/* renders as a link to <a href="/usr/123">Link Text</a> with the text */
251 | 253 | Now you have links, but your links always stay up to date with your 254 | routing configuration. 255 |
256 |