Skip to content

Instantly share code, notes, and snippets.

@sebald
Forked from jayphelps/a.js
Created November 8, 2017 18:39
Show Gist options
  • Select an option

  • Save sebald/05184c28bbbaa0ab6273f2cfcdbb05f6 to your computer and use it in GitHub Desktop.

Select an option

Save sebald/05184c28bbbaa0ab6273f2cfcdbb05f6 to your computer and use it in GitHub Desktop.
Making abstractions for redux and redux-observable
// WARNING: Completely untested code. it might not work and/or it might have
// things that don't work well. Just made for illustrational purposes
// redux-observable shines the most with complex async stuff, like WebSockets
// but many of us will still use it for more modest things like AJAX requests.
// In these cases, there can be a ton of repetitive boilerplate. So this is a
// simple example of applying some abstractions and conventions to make it easier.
// THAT SAID, since abstractions cause indirection it can make it harder for
// someone to come along later and know how something works. Weigh the costs
// and remember, this example isn't a suggestion of the actual code you should
// be using :o)
// Say this is the pattern you use all the time:
const FETCH_USER = 'FETCH_USER';
const FETCH_USER_FULFILLED = 'FETCH_USER_FULFILLED';
const FETCH_USER_REJECTED = 'FETCH_USER_REJECTED';
const FETCH_USER_CANCELLED = 'FETCH_USER_CANCELLED';
const fetchUser = id => ({ type: FETCH_USER, id });
const fetchUserCancelled = id => ({ type: FETCH_USER_CANCELLED, id });
const fetchUserFulfilled = response => ({ type: FETCH_USER_FULFILLED, response });
const fetchUserRejected = error => ({ type: FETCH_USER_REJECTED, error });
const fetchUserEpic = (action$, store) =>
action$.ofType(FETCH_USER)
.mergeMap(action =>
ajax(`/api/users/${action.payload}`)
.map(response => fetchUserFulfilled(response))
.catch(error => Observable.of(
fetchUserRejected(error)
))
.takeUntil(action$.ofType(FETCH_USER_CANCELLED))
);
const users = (state = {}, action) => {
switch (action.type) {
case FETCH_USER:
return {
...state,
[action.id]: {
...state[action.id],
isLoading: true,
error: null
}
};
case FETCH_USER_CANCELLED:
return {
...state,
[action.id]: {
...state[action.id],
isLoading: false
}
};
case FETCH_USER_FULFILLED:
return {
...state,
[action.id]: {
isLoading: false,
error: null,
payload: action.payload
}
};
case FETCH_USER_REJECTED:
return {
...state,
[action.id]: {
isLoading: false,
error: action.error,
payload: null
}
};
}
};
// Epics and reducers are just functions, so we can abstract
// all the things with a factory. We can use convention too
// if that's your bag. YMMV
const createFetchHandler = ({ name, url: urlTemplate, concurrency = 'merge' }) => {
const FETCH = `FETCH_${name}`;
const FETCH_CANCELLED = `FETCH_${name}_CANCELLED`;
const FETCH_FULFILLED = `FETCH_${name}_FULFILLED`;
const FETCH_REJECTED = `FETCH_${name}_REJECTED`;
const fetch = id => ({ type: FETCH, id });
const cancel = id => ({ type: FETCH_CANCELLED, id });
const fulfill = (id, response) => ({ type: FETCH_FULFILLED, id, response });
const reject = (id, error) => ({ type: FETCH_REJECTED, id, error });
// e.g. mergeMap, switchMap, concatMap, etc
const concurrencyOperator = `${concurrency}Map`;
const epic = (action$, store) =>
action$.ofType(FETCH)
[concurrencyOperator](action => {
// allows things like `/api/users/:id` where `id` will get looked up
const url = urlTemplate.replace(/:([a-zA-Z]+)/g, (match, key) => action[key]);
return ajax(url)
.map(response => fulfill(action.id, response))
.catch(error => Observable.of(
reject(id, error)
))
.takeUntil(action$.ofType(FETCH_CANCELLED))
});
const reducer = (state = {}, action) => {
switch (action.type) {
case FETCH:
return {
...state,
[action.id]: {
...state[action.id],
isLoading: true,
error: null
}
};
case FETCH_CANCELLED:
return {
...state,
[action.id]: {
...state[action.id],
isLoading: false
}
};
case FETCH_FULFILLED:
return {
...state,
[action.id]: {
isLoading: false,
error: null,
payload: action.payload
}
};
case FETCH_REJECTED:
return {
...state,
[action.id]: {
isLoading: false,
error: action.error,
payload: null
}
};
default:
return state;
}
};
return { epic, reducer, fetch, cancel, fulfill, reject };
};
export const { epic, reducer, fetch, cancel } = createFetchHandler({
name: 'USER',
url: '/api/users/:id',
concurrency: 'merge' // or 'switch', 'concat', etc
});
/* Setting up redux middleware using it is straight forward */
import * as users from './users';
import * as todos from './todos';
const rootReducer = combineReducers({
users: users.reducer,
todos: todos.reducer
// ...etc
});
const rootEpic = combineEpics(
users.epic,
todos.epic
// ...etc
);
/* Then we can use it in our UI later like this: */
store.dispatch(users.fetch(123));
// etc
store.dispatch(users.cancel(123));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment