Skip to content

Instantly share code, notes, and snippets.

@tracker1
Last active July 8, 2019 12:48
Show Gist options
  • Save tracker1/d6f0bab5634f0f50128c06ecbd134c80 to your computer and use it in GitHub Desktop.
Save tracker1/d6f0bab5634f0f50128c06ecbd134c80 to your computer and use it in GitHub Desktop.
Simple pattern for loading async data with react, redux and redux-thunk middleware
import api from '../api';
import { ACTION } from './constants';
// action creator for LOADERR
export const loadError = error => ({ type: ACTION.LOADERR, payload: error });
export const loading = key => ({ type: ACTION.LOADING, payload: { key } });
export const loaded = (key, data) => ({ type: ACTION.LOADED, payload: { key, data } });
// this action creator assumes that redux-thunk middleware is loaded.
export const load = recordId => async (dispatch, getState) => {
try {
// set loading state
dispatch(loading(recordId));
// fetch the actual data
const data = await api.getRecord(recordId);
// get the currently set loading record in state
const ({ loading }) = getState();
// if the record isn't the current one - don't do anything else
if (loading !== recordId) return;
// set the loaded record to the returned data
dispatch(loaded(recordId, data));
} catch (error) {
// if an error occurred, log it to the console, and dispatch it out
console.error(error);
dispatch(loadError(error));
}
}
export ACTION = {
LOADING: 'PREFIX:LOADING',
LOADED: 'PREFIX:LOADED',
LOADERR: 'PREFIX:LOADERROR',
};
export DEFAULT_STATE = {
loading: null,
loaded: NaN, // never match
data: {},
error: null,
};
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as Actions from './action';
const mapStateToProps = ({ loaded, loading, error, data }) = ({ loaded, loading, error, data });
const mapDispatchToProps = dispatch => ({ action: bindActionCreators(Actions, dispatch) });
export class MyComponent extends Component {
load = _ => this.props.action.load(this.props.id);
componentDidMount = _ => this.componentDidUpdate();
componentDidUpdate = _ => {
// id passed in as property/attribute
const { id, loading, error } = this.props;
// there was an error, nothing to do
if (error) return;
// currently loading or loaded
if (id === loading) return;
this.load(id);
};
render() {
const { id, loading, loaded, data, error } = this.props;
if (error) return (
<div>Error! <a onClick={this.load}>Retry</a></div>
);
if (id != loading && loading !== loaded) return (
<div>Loading...</div>
)
return (
<div>{data.id} Loaded!</div>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);
import clone from 'fclone';
import { ACTION, DEFAULT_STATE } from './constants';
export default (state = clone(DEFAULT_STATE), action) {
switch (action.type) {
case '@@INIT': // redux initialization - page refresh
case '@@router/LOCATION_CHANGE': // connected-react-router
// when reloaded/rerouted during a data load, reset this portion of state
if (state.loading && state.loading !== state.loaded) {
return clone(DEFAULT_STATE);
}
// NOTE: if this reducer/state section is only valid in certain routes,
// you may want to check location.pathname here too.
return state;
case ACTION.LOADING:
return { ...DEFAULT_STATE, loading: action.payload.key };
case ACTION.LOADED:
return { ...DEFAULT_STATE, loading: action.payload.key, loaded: action.payload.key, data: action.payload.data };
case ACTION.LOADERROR:
return { ...DEFAULT_STATE, error: action.payload;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment