marcandrewb
3/14/2016 - 5:47 PM

Reusable action creators / reducers.

Reusable action creators / reducers.

import FetchActionCreators from './FetchActionCreators';
import FetchReducerPrototype from './FetchReducerPrototype';
import ReducerChain from './ReducerChain';

const REQUEST_VIDEOS = 'REQUEST_VIDEOS';
const RECEIVE_VIDEOS_SUCCESS = 'RECEIVE_VIDEOS_SUCCESS';
const RECEIVE_VIDEOS_FAILURE = 'RECEIVE_VIDEOS_FAILURE';
const MODIFY_VIDEO_DATA = 'MODIFY_VIDEO_DATA';

export const videosFetchActions = new FetchActionCreators(
  'http://yourwebsite.com/videos',
  [
    REQUEST_VIDEOS,
    RECEIVE_VIDEOS_SUCCESS,
    RECEIVE_VIDEOS_FAILURE,
  ]
);

const fetchReducerPrototype = new FetchReducerPrototype(videosFetchActions);

const videoModifyingReducer(state, action) {
  switch (action.type) {
    case MODIFY_VIDEO_DATA:
      return {
        ...state, 
        {
          data: {
            ...state.data,
            someProp: action.someProp,
          }
        }
      });  
      
    default:
      return state;
  }
}

export function modifyVideoData(someValue) {
  return {
    type: MODIFY_VIDEO_DATA,
    someProp: someValue,
  }
}

export default new ReducerChain([
  fetchReducerPrototype,
  videoModifyingReducer,
]);
import React, { PropTypes } from 'react';
import { videosFetchActions, modifyVideoData } from './VideosReducer';

class VideosContainer extends React.Component {
  static propTypes = {
    dispatch: PropTypes.func.isRequired,
    videos: PropTypes.object.isRequired,
  };
  
  componentDidMount() {
    const { dispatch, videos } = this.props;
    if (!videos.fetching && !videos.data) {
      dispatch(videosFetchActions.fetch());
    }
  }
  
  render() {
    const { dispatch, videos } = this.props;
    return videos.fetching ? 
      <div>Loading...</div> : 
      (
        <div>
          {/* access videos via videos.data */}
          <button onClick={(e) => dispatch(modifyVideoData('Some value'))} />
        </div>
      );
  }
}

function mapStateToProps(state) {
  return {
    videos: state.videos,
  }
}

export default connect(mapStateToProps)(VideosContainer);
import FetchActionCreators from './FetchActionCreators';
import FetchReducerPrototype from './FetchReducerPrototype';

const REQUEST_STREAMS = 'REQUEST_STREAMS';
const RECEIVE_STREAMS_SUCCESS = 'RECEIVE_STREAMS_SUCCESS';
const RECEIVE_STREAMS_FAILURE = 'RECEIVE_STREAMS_FAILURE';

export const streamsFetchActions = new FetchActionCreators(
  'http://yourwebsite.com/streams',
  [
    REQUEST_STREAMS,
    RECEIVE_STREAMS_SUCCESS,
    RECEIVE_STREAMS_FAILURE,
  ]
);

export default new FetchReducerPrototype(streamsFetchActions);
import React, { PropTypes } from 'react';
import { streamsFetchActions } from './StreamsReducer';

class StreamsContainer extends React.Component {
  static propTypes = {
    dispatch: PropTypes.func.isRequired,
    streams: PropTypes.object.isRequired,
  };
  
  componentDidMount() {
    const { dispatch, streams } = this.props;
    if (!streams.fetching && !streams.data) {
      dispatch(streamsFetchActions.fetch());
    }
  }
  
  render() {
    const { streams } = this.props;
    return streams.fetching ? 
      <div>Loading...</div> : 
      <div>{/* access streams via streams.data */}</div>;
  }
}

function mapStateToProps(state) {
  return {
    streams: state.streams,
  }
}

export default connect(mapStateToProps)(StreamsContainer);
import { combineReducers } from 'redux';

import StreamsReducer from './StreamsReducer';
import VideosReducer from './VideosReducer';

const rootReducer = combineReducers({
  streams: StreamsReducer,
  videos: VideosReducer,
});
export default class ReducerChain {
  constructor(reducers) {
    this.reducers = reducers;

    if (!Array.isArray(reducers)) {
      throw new Error('To create a reducer chain you must pass in an array of functions.');
    }

    return this.reducer;
  }

  reducer = (state, action) => {
    let result = state;

    for (let i = 0, len = this.reducers.length; i < len; i++) {
      result = this.reducers[i](result, action);
    }

    return result;
  };
}
class FetchReducerPrototype {
  static initialState = {
    data: null,
    fetching: false,
    receivedAt: null,
  };

  constructor({ actions }) {
    this.actions = actions;
    return this.reducer;
  }

  reducer = (state = FetchReducerPrototype.initialState, action = {}) => {
    switch (action.type) {
      case this.actions.REQUEST:
        return Object.assign({}, state, {
          fetching: true,
        });

      case this.actions.SUCCESS:
        return Object.assign({}, state, {
          data: action.data,
          fetching: false,
          receivedAt: Date.now(),
        });

      case this.actions.FAILURE:
        return Object.assign({}, state, {
          fetching: false,
        });

      default:
        return state;
    }
  }
}

export default FetchReducerPrototype;
import axios from 'axios';

class FetchActionCreators {
  constructor(endpoint, actions) {
    const [REQUEST, SUCCESS, FAILURE] = actions;

    this.actions = {
      REQUEST,
      SUCCESS,
      FAILURE,
    };
    this.endpoint = endpoint;
  }

  fetch({ endpoint, params }) {
    return (dispatch) => {
      dispatch(this.request());

      return axios.get(endpoint || this.endpoint, { params })
        .then(res => {
          dispatch(this.receive(this.actions.SUCCESS, res.data));
        })
        .catch((res) => {
          dispatch(this.receive(this.actions.FAILURE));
        });
    };
  }

  request() {
    return {
      type: this.actions.REQUEST,
    };
  }

  receive(type, data) {
    return {
      type,
      data,
    };
  }
}

export default FetchActionCreators;