How to make AJAX request in redux

64,597

Solution 1

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 };
}

Solution 2

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.

Solution 3

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;
  }
};

Solution 4

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.

Share:
64,597
Admin
Author by

Admin

Updated on January 24, 2022

Comments

  • Admin
    Admin over 2 years

    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;
    
  • Admin
    Admin over 8 years
    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.
  • Davin Tryon
    Davin Tryon over 8 years
    Did you reduce the action.data into state?
  • Admin
    Admin over 8 years
    Object.assign({}, state, { books: action.data.books, authors: action.data.authors });
  • Davin Tryon
    Davin Tryon over 8 years
    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.
  • Admin
    Admin over 8 years
    I used console.log, and state.data equal 0. That mean case GET_BOOK don't work. But why?
  • Admin
    Admin over 8 years
    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