Skip to content

Instantly share code, notes, and snippets.

@msociety
Created February 27, 2020 17:44
Show Gist options
  • Save msociety/6b7da12020cc837af3fa69764d3b68bb to your computer and use it in GitHub Desktop.
Save msociety/6b7da12020cc837af3fa69764d3b68bb to your computer and use it in GitHub Desktop.
react's useReducer, combineReducers and HORs
import { useEffect, useReducer, useMemo } from 'react';
import { not, equals } from 'ramda';
import usePrevious from '../../../hooks/usePrevious';
import { getSitesFromCache, getSitesFromNetwork } from './getSites';
const areDifferent = (arr1, arr2) => not(equals(arr1, arr2));
const getIds = items => (items ? items.map(({ id }) => id) : []);
const defalutStatus = {
loading: false,
loaded: false,
error: null,
sites: null
};
const combineReducers = reducers => {
const storeKeys = Object.keys(reducers);
return (state = {}, action) =>
storeKeys.reduce((acc, key) => ({ ...acc, [key]: reducers[key](state[key], action) }), {});
};
const createFilteredReducer = (reducerFunction, reducerPredicate) => (state, action) => {
const isInitializationCall = state === undefined;
const shouldRunWrappedReducer = reducerPredicate(action) || isInitializationCall;
return shouldRunWrappedReducer ? reducerFunction(state, action) : state;
};
// Actions:
const INITIALIZE = 'INITIALIZE';
const PENDING = 'PENDING';
const REJECTED = 'REJECTED';
const FULFILLED = 'FULFILLED';
const reducer = (state, { type, payload }) => {
// console.log('reducer', type, payload);
switch (type) {
case INITIALIZE:
return payload.instanceIds.map(instanceId => ({
...defalutStatus,
instanceId
}));
case PENDING:
return state.map(item =>
item.instanceId === payload.instanceId
? {
...item,
...defalutStatus,
loading: true
}
: item
);
case REJECTED:
return state.map(item =>
item.instanceId === payload.instanceId
? {
...item,
...defalutStatus,
error: payload.error
}
: item
);
case FULFILLED:
return state.map(item =>
item.instanceId === payload.instanceId
? {
...item,
...defalutStatus,
loaded: true,
sites: payload.sites || []
}
: item
);
default:
console.error('Unexpected action');
return state;
}
};
const KEY_CACHE = 'cache';
const KEY_NETWORK = 'network';
const defaultState = {
[KEY_CACHE]: [],
[KEY_NETWORK]: []
};
const rootReducer = combineReducers({
[KEY_CACHE]: createFilteredReducer(reducer, action => action.key === KEY_CACHE),
[KEY_NETWORK]: createFilteredReducer(reducer, action => action.key === KEY_NETWORK)
});
const getMergedStates = (cacheState, ntwrkState) => ({
instanceId: ntwrkState.instanceId || cacheState.loading,
loading: ntwrkState.loading || cacheState.loading,
loaded: ntwrkState.loaded || cacheState.loaded,
error: ntwrkState.error || cacheState.error,
sites: ntwrkState.sites || cacheState.sites
});
const useSites = instances => {
const [{ [KEY_CACHE]: cacheSites, [KEY_NETWORK]: networkSites }, dispatch] = useReducer(
rootReducer,
defaultState
);
const initialize = (key, instanceIds) =>
dispatch({ key, type: INITIALIZE, payload: { instanceIds } });
const setPending = (key, instanceId) => dispatch({ key, type: PENDING, payload: { instanceId } });
const setRejected = (key, instanceId, error) =>
dispatch({ key, type: REJECTED, payload: { instanceId, error } });
const setFulfilled = (key, instanceId, sites) =>
dispatch({ key, type: FULFILLED, payload: { instanceId, sites } });
const prevInstances = usePrevious(instances);
const getCacheSites = async instanceIds => {
return Promise.all(
instanceIds.map(instanceId => {
setPending(KEY_CACHE, instanceId);
return getSitesFromCache(instanceId)
.then(sites => setFulfilled(KEY_CACHE, instanceId, sites))
.catch(error => setRejected(KEY_CACHE, instanceId, error));
})
);
};
const getNtwrkSites = instanceIds => {
instanceIds.forEach(instanceId => {
setPending(KEY_NETWORK, instanceId);
getSitesFromNetwork(instanceId)
.then(sites => setFulfilled(KEY_NETWORK, instanceId, sites))
.catch(error => setRejected(KEY_NETWORK, instanceId, error));
});
};
const refetch = instanceId =>
instanceId ? getNtwrkSites([instanceId]) : getNtwrkSites(getIds(instances));
useEffect(() => {
const instanceIds = getIds(instances);
if (instanceIds.length > 0) {
const prevInstanceIds = getIds(prevInstances);
if (prevInstanceIds.length === 0) {
initialize(KEY_CACHE, instanceIds);
getCacheSites(instanceIds).then(() => {
initialize(KEY_NETWORK, instanceIds);
getNtwrkSites(instanceIds);
});
} else if (areDifferent(prevInstanceIds, instanceIds)) {
getNtwrkSites(instanceIds);
}
}
}, [instances]);
const instanceSites = useMemo(() => {
if (cacheSites.some(({ loading }) => loading)) return cacheSites;
return getIds(instances).map(instanceId => {
const isSameId = item => item.instanceId === instanceId;
const cacheState = cacheSites.find(isSameId);
const ntwrkState = networkSites.find(isSameId);
return ntwrkState ? getMergedStates(cacheState, ntwrkState) : cacheState;
});
}, [cacheSites, networkSites]);
return {
instanceSites,
refetch
};
};
export default useSites;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment