Async actions in Redux
Asked Answered
B

4

33

I have a React App, I need to make an ajax call (in order to learn) to a online service (async) with Redux.

This is my store:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import duedates from './reducers/duedates'


export default applyMiddleware(thunk)(createStore)(duedates);

This is the actions:

import rest from '../Utils/rest';

export function getDueDatesOptimistic(dueDates){
    console.log("FINISH FETCH");
    console.log(dueDates);
    return {
        type: 'getDueDate',
        dueDates
    }
}

export function waiting() {
    console.log("IN WAIT");
    return {
        type: 'waiting'
    }
}


function fetchDueDates() {
    console.log("IN FETCH");
    return rest({method: 'GET', path: '/api/dueDates'});
}

export function getDueDates(dispatch) {
    console.log("IN ACTION");
    return fetchDueDates().done(
        dueDates => dispatch(getDueDatesOptimistic(dueDates.entity._embedded.dueDates))
    )
}

And this is the reducer:

export default (state = {}, action) => {
  switch(action.type) {
    case 'getDueDate':
        console.log("IN REDUCER")

        return state.dueDates = action.dueDates;
    default:
        return state
  }
}

I dont get what I'm doing wrong. The action is being called perfectly from the component. But then I get this error:

Error: Actions must be plain objects. Use custom middleware for async actions.

I guess I'm using wrong the react-thunk middleware. What am I doing wrong?

EDIT

Now the action is calling to the reducer, but the reducer, after changing state, is not re-running the render method

    case 'getDueDate':
        console.log("IN REDUCER")

        return state.dueDates = action.dueDates;
Berenice answered 20/2, 2016 at 23:51 Comment(0)
C
31

I think you should be using compose function, so it's like

import {
  createStore,
  applyMiddleware,
  compose
} from 'redux';
import thunk from 'redux-thunk';
import duedates from './reducers/duedates'

export default compose(applyMiddleware(thunk))(createStore)(duedates);

Thunk allows an action creator to return a function instead of plain-object, so you use it like

export function getDueDates() {
  return dispatch => {
    console.log("IN ACTION");
    fetchDueDates().done(
      dueDates => dispatch(getDueDatesOptimistic(dueDates.entity._embedded.dueDates))
    )
  };
}

You were returning a Promise object, that was one part of the problem. Another part was that redux-thunk hasn't been properly applied. I bet compose should get the problem solved.

Consortium answered 21/2, 2016 at 0:52 Comment(6)
Great, seems to be working now. but in the reducer, when I return the state return state.dueDates = action.dueDates;, it is not calling render again.. Should I add something else?Berenice
Right now, in your code sample, both actions don't change state. You return state itself. Can you update your questions so I (or anybody else) could see the current situation?Consortium
updated, now I return state.dueDates = action.dueDates;Berenice
You shouldn't be doing that. This expression returns the value of the right part, which is not the way you want to reduce your state. Instead, try to use object spread: return {...state, dueDates: action.dueDates}.Consortium
Yes! that was it. Apparently, I must return a different state object, and not adding new data to the sameBerenice
ugghh, sorry Redux, but this is not pretty at all....really "export default compose(applyMiddleware(thunk))(createStore)(duedates);" ?Anitraaniweta
A
17

The accepted answer is either outdated, wrong or over-convoluted. Here are the docs on the compose subject:

http://redux.js.org/docs/api/compose.html

so we can do it like this instead:

import {createStore, combineReducers, compose, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';

const reducer = combineReducers({
    user: userReducer,
    items: itemsReducer
});


// here is our redux-store
const store = createStore(reducer,
    compose(applyMiddleware(thunk))
);
Anitraaniweta answered 22/1, 2017 at 1:56 Comment(0)
C
1

I believe it is possible to have a working solution without using the compose function too. Based on the documentation from the GitHub repo for redux-thunk

Solution works for redux-thunk: ^2.0.0

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import duedates from './reducers/duedates'

export function configureStore(initialState = {}) {
  return createStore(
    duedates,
    initialState,
    applyMiddleware(thunk)
  );
}
Coffeehouse answered 18/10, 2017 at 6:38 Comment(4)
Hey, Kevin, but where does createReducer come from?Consortium
HI @RishatMuhametshin, I have corrected the example above. createReducer was a function to avoid boilerplate code and taken from react-boilerplateCoffeehouse
This worked for me. As stated by Kevin, compose was not required.Houlihan
Awwwww!!!!! [scratching head] I ran into this issue because I missed the semicolon in the import of import thunkMiddleware from 'redux-thunk'.Houlihan
L
0

I see multiple answers with differing syntax. We'll I'm going to propose a solution without having to apply middlewares manually & without having to install redux-thunk separately.

The sole redux dependency: @reduxjs/toolkit: ^1.8.6

src/redux/store.js

import { configureStore } from "@reduxjs/toolkit";
import category from './features/category.feature';

const store = configureStore({
    reducer: {
        category
    }
});

export default store;

src/redux/features/category.feature.js

import { createSlice } from "@reduxjs/toolkit";
import { XHR_STATUS } from "../../misc/enums";
import axios from 'axios';
import { BASE_URL } from "../../misc/constants/xhr";

const initialState = {
    data: [],
    status: XHR_STATUS.NEUTRAL,
    errorMessage: ''
};

let slice = createSlice({
    name: 'category',
    initialState,
    reducers: {
        setStatus: (state, actions) => {
            state.status = actions.payload;
        },
        setData: (state, action) => {
            state.data = action.payload;
        }
    }
});

export const { setStatus, setData } = slice.actions;


export const fetchCategories = () => {
    return async (dispatch) => {
        dispatch(setStatus(XHR_STATUS.LOADING));
        const response = await axios.get(`${BASE_URL}categories`);
        const { data } = response;
        dispatch(setData(data));
        dispatch(setStatus(XHR_STATUS.SUCCESS));
    }
}

export default slice.reducer;

src/components/Categories/index.js

import { XHR_STATUS } from '../../misc/enums';
import { ListItem, List } from '@mui/material';
import CategoryBox from '../CategoryBox'; // This is my custom component to render a single Category
function Categories (props) {
    const { category } = props;
    switch(category.status) {
        case XHR_STATUS.NEUTRAL:
            return (
                'Waitng...'
            );
        case XHR_STATUS.LOADING:
            return (
                'Loading...'
            );
        case XHR_STATUS.SUCCESS:
            return (
                <List style={{ padding: '20px 0 20px 0', display: 'inline-flex', flexDirection: 'row', overflowX: 'scroll', width: '100%' }}>
                {category.data.map((c, i) => (  
                    <ListItem key={i}>
                        <CategoryBox category={c} />
                    </ListItem>
                ))}
                </List> 
            );
        case XHR_STATUS.ERROR:
            return (
                'Error...'
            );
    }
}
Lychnis answered 18/12, 2022 at 19:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.