67

For all I know, I have to write request in action create. How to use a promise in action for submitting a request? I am getting data in action. Then new state is created in reducer. Bind action and reducer in connect. But I don't know how to use promise for request.

Action

import $ from 'jquery';
export const GET_BOOK = 'GET_BOOK';

export default function getBook() {
  return {
    type: GET_BOOK,
    data: $.ajax({
      method: "GET",
      url: "/api/data",
      dataType: "json"
    }).success(function(data){
      return data;
    })
  };
}

Reducer

import {GET_BOOK} from '../actions/books';

const booksReducer = (state = initialState, action) => {
  switch (action.type) {
    case GET_BOOK:
      return state;
    default:
      return state;
  }
};

export default booksReducer;

Container How display data in container?

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import getBook  from '../actions/books';
import Radium from 'radium';
import {Link} from 'react-router';

function mapStateToProps(state) {
  return {
    books: state.data.books,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    getBooks: () => dispatch(getBook()),
  };
}

@Radium
@connect(mapStateToProps, mapDispatchToProps)
class booksPage extends Component {
  static propTypes = {
    getBooks: PropTypes.func.isRequired,
    books: PropTypes.array.isRequired,
  };

  render() {
    const {books} = this.props;
    return (
      <div>
        <Link to={`/authors`}><MUIButton style="flat">All Authors</MUIButton></Link>
        <ul>
          {books.map((book, index) =>
            <li key={index}>
              <Link to={`/book/${book.name}`}><MUIButton style="flat"><div class="mui--text-black mui--text-display4">
                "{book.name}"</div></MUIButton></Link>
              <Link to={`/author/${book.author}`}><MUIButton style="flat"><div class="mui--text-black mui--text-display4">
                {book.author}</div></MUIButton></Link>
            </li>
          )}
        </ul>
      </div>
    );
  }
}

export default booksPage;
akashrajkn
  • 2,295
  • 2
  • 21
  • 47
Disa Skolzin
  • 691
  • 1
  • 5
  • 4

5 Answers5

56

Since you are already using redux you can apply redux-thunk middleware which allows you to define async actions.

Installation & usage: Redux-thunk

export function fetchBook(id) {
 return dispatch => {
   dispatch(setLoadingBookState()); // Show a loading spinner
   fetch(`/book/${id}`, (response) => {
     dispatch(doneFetchingBook()); // Hide loading spinner
     if(response.status == 200){
       dispatch(setBook(response.json)); // Use a normal function to set the received state
     }else { 
       dispatch(someError)
     }
   })
 }
}

function setBook(data) {
 return { type: 'SET_BOOK', data: data };
}
Jens
  • 1,132
  • 7
  • 14
30

You should use Async Actions described in Redux Documentation

Here an example of reducer for async action.

const booksReducer = (state = {}, action) => {
  switch (action.type) {
    case 'RESOLVED_GET_BOOK':
      return action.data;
    default:
      return state;
  }
};

export default booksReducer;

and then you create your Async Action.

export const getBook() {
  return fetch('/api/data')
    .then(response => response.json())
    .then(json => dispatch(resolvedGetBook(json)))
}

export const resolvedGetBook(data) {
  return {
    type: 'RESOLVED_GET_BOOK',
    data: data
  }
}

Several Notes:

  • We could return Promise (instead of Object) in action by using redux-thunk middleware.
  • Don't use jQuery ajax library. Use other library specifically for doing that (e.g. fetch()). I use axios http client.
  • Remember, in redux you only use pure function in reducer. Don't make ajax call inside reducer.
  • Read the complete guide from redux docs.
nonopolarity
  • 146,324
  • 131
  • 460
  • 740
Kiddo
  • 1,167
  • 1
  • 12
  • 23
14

You should be able to use dispatch inside the callback (if you pass it as an argument):

export default function getBook(dispatch) {
  $.ajax({
      method: "GET",
      url: "/api/data",
      dataType: "json"
    }).success(function(data){
      return dispatch({type:'GET_BOOK', data: data});
    });
}

Then, pass dispatch to the action:

function mapDispatchToProps(dispatch) {
  return {
    getBooks: () => getBook(dispatch),
  };
}

Now, you should have access to the action.data property in the reducer:

const booksReducer = (state = initialState, action) => {
  switch (action.type) {
    case GET_BOOK:
      //action.data  <--- here
      return state;
    default:
      return state;
  }
};
Davin Tryon
  • 66,517
  • 15
  • 143
  • 132
  • thanks, but now i get Warning: Failed propType: Required prop `books` was not specified in `booksPage`. Check the render method of `Connect(booksPage)`.warning @ (program):45 (program):45 Warning: getDefaultProps is only used on classic React.createClass definitions. Use a static property named `defaultProps` instead. – Disa Skolzin Nov 24 '15 at 11:58
  • Did you reduce the `action.data` into state? – Davin Tryon Nov 24 '15 at 12:03
  • Object.assign({}, state, { books: action.data.books, authors: action.data.authors }); – Disa Skolzin Nov 24 '15 at 12:07
  • If you reduce like that, then `state.data.books` cannot be found in your `mapStateToProps`. Do a `console.log` there and see what `state` is. – Davin Tryon Nov 24 '15 at 12:11
  • I used console.log, and state.data equal 0. That mean case GET_BOOK don't work. But why? – Disa Skolzin Nov 24 '15 at 12:22
  • I used console.log(action.type) and get @@redux/INIT, @@redux/PROBE_UNKNOWN_ACTION_o.4.d.n.q.g.d.s.4.i, @@INIT @@reduxReactRouter/routerDidChange – Disa Skolzin Nov 24 '15 at 12:30
7

You might want to separate concerns, to keep action creators "pure".

Solution; write some middleware. Take this for example (using superagent).

import Request from 'superagent';

const successHandler = (store,action,data) => {

    const options = action.agent;
    const dispatchObject = {};
    dispatchObject.type = action.type + '_SUCCESS';
    dispatchObject[options.resourceName || 'data'] = data;
    store.dispatch(dispatchObject);
};

const errorHandler = (store,action,err) => {

    store.dispatch({
        type: action.type + '_ERROR',
        error: err
    });
};

const request = (store,action) => {

    const options = action.agent;
    const { user } = store.getState().auth;
    let method = Request[options.method];

    method = method.call(undefined, options.url)

    if (user && user.get('token')) {
        // This example uses jwt token
        method = method.set('Authorization', 'Bearer ' + user.get('token'));
    }

    method.send(options.params)
    .end( (err,response) => {
        if (err) {
            return errorHandler(store,action,err);
        }
        successHandler(store,action,response.body);
    });
};

export const reduxAgentMiddleware = store => next => action => {

    const { agent } = action;

    if (agent) {
        request(store, action);
    }
    return next(action);
};

Put all this in a module.

Now, you might have an action creator called 'auth':

export const auth = (username,password) => {

    return {
        type: 'AUTHENTICATE',
        agent: {
            url: '/auth',
            method: 'post',
            resourceName: 'user',
            params: {
                username,
                password
            }
        }
    };
};

The property 'agent' will be picked up by the middleware, which sends the constructed request over the network, then dispatches the incoming result to your store.

Your reducer handles all this, after you define the hooks:

import { Record } from 'immutable';

const initialState = Record({
    user: null,
    error: null
})();

export default function auth(state = initialState, action) {

    switch (action.type) {

        case 'AUTHENTICATE':

            return state;

        case 'AUTHENTICATE_SUCCESS':

            return state.merge({ user: action.user, error: null });

        case 'AUTHENTICATE_ERROR':

            return state.merge({ user: null, error: action.error });

        default:

            return state;
    }
};

Now inject all this into your view logic. I'm using react as an example.

import React from 'react';
import ReactDOM from 'react-dom';

/* Redux + React utils */
import { createStore, applyMiddleware, bindActionCreators } from 'redux';
import { Provider, connect } from 'react-redux';

// thunk is needed for returning functions instead 
// of plain objects in your actions.
import thunkMiddleware from 'redux-thunk';

// the logger middleware is useful for inspecting data flow
import createLogger from 'redux-logger';

// Here, your new vital middleware is imported
import { myNetMiddleware } from '<your written middleware>';

/* vanilla index component */
import _Index from './components';

/* Redux reducers */
import reducers from './reducers';

/* Redux actions*/
import actionCreators from './actions/auth';


/* create store */
const store = createStore(
    reducers,
    applyMiddleware(
        thunkMiddleware,
        myNetMiddleware
    )
);

/* Taint that component with store and actions */
/* If all goes well props should have 'auth', after we are done */
const Index = connect( (state) => {

    const { auth } = state;

    return {
        auth
    };
}, (dispatch) => {

    return bindActionCreators(actionCreators, dispatch);
})(_Index);

const provider = (
    <Provider store={store}>
        <Index />
    </Provider>
);

const entryElement = document.getElementById('app');
ReactDOM.render(provider, entryElement);

All of this implies you already set up a pipeline using webpack,rollup or something, to transpile from es2015 and react, to vanilla js.

Thomas E
  • 3,808
  • 2
  • 20
  • 13
-1

Consider using the new thunk API

export const load = createAsyncThunk(
  'example/api',
  async (arg, thunkApi) => {
    const response = await fetch('http://example.api.com/api')

    if (response.status === 200) {
      const json = await response.json()
      return json
  },
)

Also, in the new redux template application, actions are part of the reducer/slice, and you can use extraReducers to response to events related to the async action status. It is much simpler using redux this way.

See documentation of async thunk here: https://redux.js.org/usage/writing-logic-thunks

Flexo
  • 87,323
  • 22
  • 191
  • 272
alonana
  • 171
  • 2
  • 12