Skip to content

Instantly share code, notes, and snippets.

@marcelaraujo
Forked from cherta/actionCreators.js
Created November 3, 2016 16:04
Show Gist options
  • Save marcelaraujo/ec7e62cc29d336bdef29f526781da2c2 to your computer and use it in GitHub Desktop.
Save marcelaraujo/ec7e62cc29d336bdef29f526781da2c2 to your computer and use it in GitHub Desktop.
redux-simple-router and sagas
export function retrieve(nodeId) {
return { type: 'RETRIEVE_NODE', payload: { nodeId: nodeId } }
}
export function show(nodes) {
return { type: 'SHOW_NODE', payload: { nodes: nodes } }
}

Sagas and redux simple router

Motivation

Once you add redux simple router (or redux router) you have two places in your state that need to be kept in synch.

import { combineReducers } from 'redux'
import { routeReducer } from 'redux-simple-router'
import tree from './tree'

//reducers/index.js

const rootReducer = combineReducers({
  tree: tree,
  routing: routeReducer
})

export default rootReducer

Now every time you change tree you need to update the routing part, that's easy because your action creators may push a new state into the browser history.

The other way around, changing the URL and reduce correctly is a bit more complex because the router dispatches only one action (UPDATE_LOCATION if using redux-simple-router) and based on this action you need to reduce your state correctly.

Matching -and sometimes parsing- the url in your reducers is something that feels wrong, it is a logic that doesn't belong to the reducer.

Sagas

Sagas in redux are an a way to listen for redux actions and spawn side effects accordingly. I've created a couple of sagas in sagas/index.js file.

The first one retriveNode watches for the RETRIEVE_NODE an action that I dispatch on the connected components, once the action is fired it executes in order some side effects, one of them being the url change provided by the push function.

Now we need to tackle another problem, what happens when someone changes the url like when I click the back or forward button on the browser?

In that case the routing part of our state changes but the tree part remains the same. The solution is in the other saga called urlChanged.

I listen to the UPDATE_LOCATION action and check if this action has been a POP, a POP action is our way to say: Did this action occur has a result of a back / forward navigation or it was intenionally pushed. This prevents executing ina loop both sagas.

Once the url changed as a result of the back / forward button we retreive the node, this will wake up the retriveNode saga and re build our state correctly.

import { routeActions, UPDATE_LOCATION } from 'redux-simple-router'
import { take, put, fork, call } from 'redux-saga'
import { show, retrieve } from './actionCreators'
/*
* Small tree structure in a plain hash that works as my backend server.
*/
const fakeDB = {
'/': [
{ id: '/1',
title: '1'
},
{ id: '/2',
title: '2'
}
],
'/1': [
{ id: '/1/a',
title: 'a'
},
{ id: '/1/b',
title: 'b'
}
],
'/1/a': [
{ id: '/1/a/I',
title: 'I'
}
]
}
/*
* This saga `take`s the RETRIEVE_NODE action, then executes in order:
* 1. Changing the URL with the redux simple router `routeActions.push` method
* 2. Request a list of nodes from the server
* 3. Once the nodes are back call `show` a custom action that will reduce the state with new nodes
*/
export function* retrieveNode() {
let action = null
while(action = yield take('RETRIEVE_NODE')) {
yield put(routeActions.push(action.payload.nodeId))
let nodes = yield call(fetchNodes, action.payload.nodeId)
yield put(show(nodes))
}
}
/*
* This saga `take`s the UPDATE_LOCATION action, then checks if the update is a push or a pop.
* A pop means hiting the back/forward button, in this case -and only in this case- I want to
* retrieve new nodes, this will put a RETRIEVE_NODE andthat will be handled by the previous saga.
*/
export function* urlChanged() {
let action = null
while(action = yield take(UPDATE_LOCATION)) {
if(action.location.action === 'POP') {
yield put(retrieve(action.location.pathname))
}
}
}
function fetchNodes (nodeId) {
return new Promise(resolve =>
setTimeout( () => resolve(fakeDB[nodeId]), 1000 )
)
}
export default function* root() {
yield fork(retrieveNode)
yield fork(urlChanged)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment