Skip to content

Instantly share code, notes, and snippets.

@jayphelps
Last active April 26, 2018 15:38
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jayphelps/0e92f3655032f9a055c475ae1d43c06b to your computer and use it in GitHub Desktop.
Save jayphelps/0e92f3655032f9a055c475ae1d43c06b 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));
@aeruhxi
Copy link

aeruhxi commented Dec 6, 2017

How do I put an extra callback for success or failure from outside, which can also dispatch actions?

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