redux-utilities / redux-promise

FSA-compliant promise middleware for Redux.
MIT License
2.67k stars 134 forks source link

Tracking progress of the promise in reducers #10

Open anatoliyarkhipov opened 9 years ago

anatoliyarkhipov commented 9 years ago

Hi. The Promise middleware is very cool, but what if we add the progress tracking too? It may looks like this:

import { isFSA } from 'flux-standard-action';

function isPromise(val) {
  return val && typeof val.then === 'function';
}

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action)
        ? action.then(dispatch)
        : next(action);
    }

    function progress(percent = 0) {
        dispatch({ ...action, payload: { percent }, progress: true });
    }

    if (isPromise(action.payload)) {

        progress();

        return action.payload.then(
          result => dispatch({ ...action, payload: result }),
          error => dispatch({ ...action, payload: error, error: true }),
          percent => progress(percent)
        );
    }

    return next(action);
  };
}

Even if the promise do not supports the progress callback, the first call of it can be useful to determine that we must display some kind of gif-spinner.

Usage example:

// Post Reducers

const displayPostReducer = handleAction('DISPLAY_POST', (state, action) => {
    // do something for display post
})

const updatePostReducer = handleAction('UPDATE_POST', (state, action) => {
    // do something for update post
})

const deletePostReducer = handleAction('DELETE_POST', (state, action) => {
    // do something for delete post
})

const progressTypes = ['DISPLAY_POST', 'UPDATE_POST', 'DELETE_POST']

const handleProgress = handleProgress(progressTypes , (state, { type, meta }) => {

    // This callback will be runned only if (action.progress == true)

    return (type == 'UPDATE_POST' || action.type == 'DELETE_POST')
        // If it's an update or delete action, then we must block form
        // only if the current post is the same with the deleted/updated post
        ? { ...state, progress: (state.post.id == meta.id) }
        : { ...state, progress: true }
})

const postReducer = (state = { post: null, progress: false }, action) => {

    state = handleProgress(state, action)

    if (state.progress) {

        return state
    }

    state = displayPostReducer(state, action)
    state = updatePostReducer(state, action)
    state = deletePostReducer(state, action)

    return state
}

// Post Actions

const buildMeta = (id) => { id }
const displayPost = createAction('DISPLAY_POST', (id) => api.getById(id), buildMeta)
const updatePost  = createAction('UPDATE_POST',  (id, data) => api.update(id, data), buildMeta)
const deletePost  = createAction('DELETE_POST',  (id) => api.delete(id), buildMeta)

// Post Components

class PostGrid extends Component {

    // ...

    render() {

        const { posts, displayPost, deletePost } = this.props

        return (
            <table>
                {_.map(posts, (post) => 

                    <tr onClick={() => displayPost(post.id)}>
                        <td>{post.title}</td>
                        <td onClick={() => deletePost(post.id)}>Delete</td>
                    </tr>
                )}
            </table>
        )
    }
}

@connect( state => ({ post: state.post, progress: state.progress }) )
class PostForm extends Component {

    // ...

    render() {

        const { progress, post } = this.props

        if (progress) {
            // display the gif-spinner 
        }

        // ...
    }
}
anatoliyarkhipov commented 9 years ago

We can also add the progress key to the reducerMap instead of using some special handleProgress function:

handleAction('FETCH_DATA', {
  next(state, action) {...}
  throw(state, action) {...}
  progress(state, action) {...}
});
rstacruz commented 8 years ago

Keep in mind though that progress is deprecated and not part of the Promises/A+ standard.

hafeez1042 commented 8 years ago

+1