import { Component } from 'react' import { createStore, combineReducers } from 'redux' import parseLinkHeader from 'parse-link-header' const START = 'start' const SUCCEED = 'succeed' const ERROR = 'error' const inflight = (state={}, action) => ( ((state) => ( action.type === START ? ( (state[action.pathname] = true, state) ) : action.type === SUCCEED || action.type === ERROR ? ( (delete state[action.pathname], state) ) : state ))({ ...state }) ) const links = (state={}, action) => ( ((state) => ( action.type === SUCCEED ? ( (state[action.pathname] = action.link, state) ) : action.type === ERROR ? ( (delete state[action.pathname], state) ) : state ))({ ...state }) ) const data = (state={}, action) => ( ((state) => ( action.type === SUCCEED ? ( (state[action.pathname] = action.data, state) ) : action.type === ERROR ? ( (delete state[action.pathname], state) ) : state ))({ ...state }) ) const errors = (state={}, action) => ( ((state) => ( action.type === ERROR ? ( (state[action.pathname] = action.error) ) : action.type === START ? ( (delete state[action.pathname], state) ) : state ))({ ...state }) ) const store = createStore(combineReducers({ inflight, data, errors, links })) const stringify = (obj) => ( Object.keys(obj).map((key) => ( `${key}=${obj[key]}` )).join('&') ) class Fetch extends Component { state = { error: null, data: null, loading: null } componentWillUnmount() { this.unmounted = true } componentDidMount() { store.subscribe(() => { if (!this.unmounted) { const { errors, inflight, data, links } = store.getState() const pathname = this.getPathname() this.setState({ error: errors[pathname] || null, data: data[pathname] || null, loading: inflight[pathname] || null, links: links[pathname] || null }) } }) this.fetch() } componentWillReceiveProps(nextProps) { const nextPathname = this.getPathname(nextProps) const currentPathname = this.getPathname() if (nextPathname !== currentPathname) { this.fetch(nextProps) } } getPathname(props) { const { url, query } = props || this.props let pathname = url if (query) pathname += '?'+stringify(query) return pathname } fetch(props) { const pathname = this.getPathname(props) const { inflight, data } = store.getState() const alreadyCached = data[pathname] const alreadyInFlight = inflight[pathname] if (!alreadyCached && !alreadyInFlight) { store.dispatch({ type: START, pathname }) fetch(pathname).then(res => { const link = res.headers.get('Link') const json = res.json() return Promise.all([ json, link ? parseLinkHeader(link) : null ]) }).then(([ data, link ]) => { store.dispatch({ type: SUCCEED, pathname, data, link }) }, (error) => ( store.dispatch({ type: ERROR, pathname, error }) )) } } render() { return this.props.children(this.state) } } export default Fetch