react-redux-fetch
Version:
A declarative and customizable way to fetch data for React components and manage that data in the Redux state
171 lines (145 loc) • 5.04 kB
Flow
// @flow
import * as React from 'react';
import { connect } from 'react-redux';
import reduce from 'lodash/reduce';
import map from 'lodash/map';
import isEqual from 'lodash/isEqual';
import forEach from 'lodash/forEach';
import memoizeOne from 'memoize-one';
import { getPromise } from '../reducers/selectors';
import capitalizeFirstLetter from '../utils/capitalizeFirstLetter';
import buildActionsFromMappings, {
ensureResourceIsObject,
validateResourceObject,
} from '../utils/buildActionsFromMappings';
import type { ReactReduxFetchResource, PromiseState, ResourceName } from '../types';
type DispatchFunctions = Object;
type Config = Array<ReactReduxFetchResource>;
type PropsFromParent = {
config: Config,
children?: Object => React.Node,
render?: Object => React.Node,
fetchOnMount?: boolean | Array<ResourceName>,
onFulfil?: (ResourceName, PromiseState<*>, DispatchFunctions) => void,
onReject?: (ResourceName, PromiseState<*>, DispatchFunctions) => void,
};
type ReduxProps = {
dispatch: Function,
fetchData: Object,
};
type Props = PropsFromParent & ReduxProps;
type State = {
dispatchFunctions: DispatchFunctions,
};
const getResourceNames = memoizeOne((config: Config) =>
map(config, (mapping: ReactReduxFetchResource) => {
const resource = ensureResourceIsObject(mapping);
validateResourceObject(resource);
return `${resource.name}`;
}),
);
class ReduxFetch extends React.Component<Props, State> {
/**
* @param {Function} dispatch Redux dispatch function
* @param {Array} mappings Array of objects with shape:
* {resource: ..., method: ..., request: ...}
* @return {Object} functions for the WrappedComponent e.g.: 'dispatchUserFetch()'
* */
static actionsFromProps = (dispatch: Function, mappings: Config): Object =>
reduce(
buildActionsFromMappings(mappings),
(actions, actionCreator, key) =>
Object.assign({}, actions, {
[ReduxFetch.getDispatchFunctionName(key)]: (...args) => {
const action = actionCreator(...args);
if (action) {
dispatch(action);
}
},
}),
{},
);
static getDispatchFunctionName = (resourceName: ResourceName) =>
`dispatch${capitalizeFirstLetter(resourceName)}`;
constructor(props: Props) {
super(props);
if (typeof props.config === 'function') {
throw new Error(
"react-redux-fetch with render props doesn't support config as a function. Use an array instead.",
);
}
this.state = {
dispatchFunctions: ReduxFetch.actionsFromProps(props.dispatch, props.config),
};
}
componentDidMount() {
const { fetchOnMount } = this.props;
const { dispatchFunctions } = this.state;
if (!fetchOnMount) {
return;
}
if (typeof fetchOnMount === 'boolean') {
forEach(dispatchFunctions, dispatchFn => dispatchFn());
return;
}
if (Array.isArray(fetchOnMount)) {
fetchOnMount.forEach((resourceName: ResourceName) =>
forEach(dispatchFunctions, (dispatchFn, fnName) => {
if (fnName.toLowerCase().includes(resourceName.toLowerCase())) {
dispatchFn();
}
}),
);
}
}
shouldComponentUpdate(nextProps: Props) {
return (
this.props.children !== nextProps.children ||
!isEqual(this.props.fetchData, nextProps.fetchData)
);
}
componentDidUpdate(prevProps: Props) {
const onFulfil = this.props.onFulfil;
const onReject = this.props.onReject;
if (onFulfil || onReject) {
map(this.props.fetchData, (repository: PromiseState<*>, key: string) => {
if (prevProps.fetchData[key].pending) {
if (onFulfil && repository.fulfilled) {
onFulfil(key, repository, this.state.dispatchFunctions);
}
if (onReject && repository.rejected) {
onReject(key, repository, this.state.dispatchFunctions);
}
}
});
}
}
render() {
const { children, render, fetchData } = this.props;
const { dispatchFunctions } = this.state;
const cb = render || children;
if (typeof cb !== 'function' && process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line
console.error(
'Warning: Must specify either a render prop, or a render function as children',
);
}
return cb({ ...fetchData, ...dispatchFunctions });
}
}
// TODO: this can probably be memoized with a custom moization function,
// this should make 'shouldComponentUpdate' obsolete
const getFetchData = (state, config: Config) =>
reduce(
getResourceNames(config),
(data, resourceName) => {
// eslint-disable-next-line no-param-reassign
data[`${resourceName}Fetch`] = getPromise(resourceName).fromState(state) || {};
return data;
},
{},
);
const mapStateToProps = (state, props: PropsFromParent) => ({
fetchData: getFetchData(state, props.config),
});
export default connect(mapStateToProps)(ReduxFetch);