Skip to content

Instantly share code, notes, and snippets.

@valscion
Last active August 29, 2015 14:24
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save valscion/45d2ecffd3aeb3ea598a to your computer and use it in GitHub Desktop.
Save valscion/45d2ecffd3aeb3ea598a to your computer and use it in GitHub Desktop.
An example of redux reducer returning a function in the state
const START = 'START';
const SUCCESS = 'SUCCESS';
const FAILURE = 'FAILURE';
const AJAX_ACTIONS_REGEX = /(.*)_(START|SUCCESS|FAILURE)$/;
const initialState = {
isLoading: () => false,
_inFlight: {}
};
function buildNewInFlight({ inFlight, actionTimestamp, actionKey, actionStatus }) {
const timestampsForAction = new Set(inFlight[actionKey]);
switch(actionStatus) {
case START:
timestampsForAction.add(actionTimestamp);
break;
case SUCCESS: // Fall-through
case FAILURE:
for (let timestamp of timestampsForAction) {
if (timestamp <= actionTimestamp) {
timestampsForAction.delete(timestamp);
}
}
break;
default:
throw new Error(`Action ${actionKey} received unknown status ${actionStatus}`);
}
return Object.assign({}, inFlight, { [actionKey]: timestampsForAction });
}
function generateIsLoading(inFlight) {
return function isLoading(actionKey) {
if (actionKey == null) {
return Object.keys(inFlight).filter(k => inFlight[k] && inFlight[k].size > 0).length > 0;
}
return !!inFlight[actionKey] && inFlight[actionKey].size > 0;
};
}
export default function loading(state = initialState, action) {
const ajaxMatch = action.type.match(AJAX_ACTIONS_REGEX);
if (!ajaxMatch) {
return state;
}
const actionKey = ajaxMatch[1];
const actionStatus = ajaxMatch[2];
console.log('Acting upon', actionKey, '— status changed to', actionStatus);
const newInFlight = buildNewInFlight({
inFlight: state._inFlight,
actionTimestamp: action.timestamp,
actionKey,
actionStatus
});
return Object.assign({}, state, {
_inFlight: newInFlight,
isLoading: generateIsLoading(newInFlight)
});
}
// BONUS: Tests for the loading reducer!
import loading from 'reducers/loading';
import { createStore } from 'redux';
describe('loading reducer', () => {
let store;
beforeEach(() => {
store = createStore(loading);
});
function runIsLoadingTestsForFunction({actionKey, getResult}) {
const itReturns = (result) => it(`returns ${result}`, () => expect(getResult()).to.eq(result));
const dispatchActions = (...actions) => {
beforeEach(() => {
actions.forEach((action) => store.dispatch(action));
});
};
describe('and no async action has started', () => {
itReturns(false);
});
describe('an async action has started', () => {
dispatchActions({ type: `${actionKey}_START`, timestamp: 100 });
itReturns(true);
describe('and finished successfully', () => {
beforeEach(() => {
store.dispatch({ type: `${actionKey}_SUCCESS`, timestamp: 100 });
});
itReturns(false);
});
describe('and finished with failure', () => {
dispatchActions({ type: `${actionKey}_FAILURE`, timestamp: 100 });
itReturns(false);
});
});
describe('two async actions have started', () => {
const olderTimestamp = 100;
const newerTimestamp = 200;
dispatchActions(
{ type: `${actionKey}_START`, timestamp: olderTimestamp },
{ type: `${actionKey}_START`, timestamp: newerTimestamp }
);
describe('and the older one has finished', () => {
describe('with a success', () => {
dispatchActions({ type: `${actionKey}_SUCCESS`, timestamp: olderTimestamp });
itReturns(true);
describe('and the newer one finished successfully', () => {
dispatchActions({ type: `${actionKey}_SUCCESS`, timestamp: newerTimestamp });
itReturns(false);
});
describe('and the newer one finished with failure', () => {
dispatchActions({ type: `${actionKey}_FAILURE`, timestamp: newerTimestamp });
itReturns(false);
});
});
describe('with a failure', () => {
dispatchActions({ type: `${actionKey}_FAILURE`, timestamp: olderTimestamp });
itReturns(true);
describe('and the newer one finished successfully', () => {
dispatchActions({ type: `${actionKey}_SUCCESS`, timestamp: newerTimestamp });
itReturns(false);
});
describe('and the newer one finished with failure', () => {
dispatchActions({ type: `${actionKey}_FAILURE`, timestamp: newerTimestamp });
itReturns(false);
});
});
});
describe('and the newer one finished successfully, while the older one is in-flight', () => {
dispatchActions({ type: `${actionKey}_SUCCESS`, timestamp: newerTimestamp });
itReturns(false);
describe('and the older one finished successfully', () => {
dispatchActions({ type: `${actionKey}_SUCCESS`, timestamp: olderTimestamp });
itReturns(false);
});
describe('and the older one finished with failure', () => {
dispatchActions({ type: `${actionKey}_FAILURE`, timestamp: olderTimestamp });
itReturns(false);
});
});
describe('and the newer one finished with failure, while the older one is in-flight', () => {
dispatchActions({ type: `${actionKey}_FAILURE`, timestamp: newerTimestamp });
itReturns(false);
describe('and the older one finished successfully', () => {
dispatchActions({ type: `${actionKey}_SUCCESS`, timestamp: olderTimestamp });
itReturns(false);
});
describe('and the older one finished with failure', () => {
dispatchActions({ type: `${actionKey}_FAILURE`, timestamp: olderTimestamp });
itReturns(false);
});
});
});
}
describe('isLoading() without arguments', () => {
runIsLoadingTestsForFunction({
actionKey: 'ANY_ACTION',
getResult: () => store.getState().isLoading()
});
});
describe('isLoading() with an argument specifying the action key', () => {
runIsLoadingTestsForFunction({
actionKey: 'SOME_ACTION',
getResult: () => store.getState().isLoading('SOME_ACTION')
});
describe('an async action has started for a different key', () => {
beforeEach(() => {
store.dispatch({ type: 'OTHER_ACTION_START', timestamp: 100 });
});
it('returns false', () => {
expect(store.getState().isLoading('SOME_ACTION')).to.be.false;
});
});
});
});
// An example action which can be launched, that the loading reducer can act upon
import myWebApi from 'api/myWebApi';
export function changeStatus(newStatus) {
return (dispatch, getState) => {
const timestamp = Date.now();
const stuffId = getState().stuff.id;
dispatch({
type: ActionTypes.CHANGE_STATUS_START,
timestamp,
newStatus
});
myWebApi
.updateStuff({ id: stuffId, status: newStatus })
.then(
() => {
dispatch({
type: ActionTypes.CHANGE_STATUS_SUCCESS,
timestamp,
newStatus
});
},
(errorJSON) => {
dispatch({
type: ActionTypes.CHANGE_STATUS_FAILURE,
timestamp,
newStatus,
error: errorJSON
});
}
);
};
}
import { connect } from 'redux/react';
import { ActionTypes } from 'constants/myConstants';
const { CHANGE_STATUS } = ActionTypes;
@connect(reduxState => ({
// Query for specific action
statusIsLoading: reduxState.loading.isLoading(ActionTypes.CHANGE_STATUS),
// Query for any action
isLoading: reduxState.loading.isLoading()
}))
export default class MyComponent extends React.Component {
render() {
const {
statusIsLoading,
isLoading
} = this.props;
return (
<div>
{isLoading && <strong>Some AJAX request is in progress...</strong>}
{statusIsLoading && <span>Hey, CHANGE_STATUS request is loading!</span>}
</div>
);
}
}
@madebyherzblut
Copy link

Thanks for posting this 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment