Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
518 views
in Technique[技术] by (71.8m points)

redux thunk - How do I resolve "Actions must be plain objects. Use custom middleware for async actions.]"?

So I have wasted 5 hours on this.

I have a redux thunk action like this:

    export const fetchUser = () => async (getState, dispatch) => {
      if (getIsFetching(getState().user)) {
        return Promise.resolve();
      }
    
      dispatch(fetchUserRequest());
    
      try {
        const response = await api.fetchUser();
    
        dispatch(fetchUserSuccess({ userObject: { ...response } }));
      } catch (error) {
        dispatch(fetchUserFailure({ message: "Could not fetch user profile." }));
      }
    };

Calling this always ended up in Actions must be plain objects. Use custom middleware for async actions.].

Yeah, sure. I'm already using redux-thunk for that, why does it keep bugging me?

NOTE: fetchUserRequest(), fetchUserSuccess() and fetchUserFailure() all return simple, plain redux actions.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Understanding this error message is key to understanding a lot of stuff in the world of Redux. This may even be an interview question you get asked in the future.

In reality, there are two things wrong with your action creator. The first thing wrong with your action creator is that your action creator is supposed to return plain JavaScript objects with a type property and optionally a payload property as well, but at present you are not returning an action from your action creator.

You might look at your code editor and look at the action creator and you may be thinking, are you looking at the same action creator as I am? It might look like you are returning an object with a type property, but in fact you are not.

Even though it looks like you are returning a JavaScript object, that is not the case.

Much of the code we write inside our editor is ES2015, 2016, 2017, 2018 and so on. The code you and I write gets transpiled down to es2015 syntax and thats what actually gets executed inside the browser.

So even though this function looks like its returning an object with a type property, in fact, after we transpile this to es2015 code, we are not.

Drop your asynchronous action creator into babeljs.io next time and you will see what I mean.

This is what is actually transpiling our code down to ES2015.

So inside of the code editor, you think you are executing the code you wrote but in fact, because you specifically have this async/await syntax, the entire function gets expanded to what you see on the right hand side of babeljs.io.

So when I say to you that your action creator is not returning a plain JavaScript object, its because you have that async/await syntax. Thats why your action creator is not working as expected.

So you return, not your action object when this is initially called. When your action creator gets called for the first time, you do not return the action object, instead, as you saw, you have some code inside that returns your request object. That is what gets returned -- a request. You return the request from your action creator and that goes into the store.dispatch method.

Then the redux store looks at what was returned and says okay, is this a plain JavaScript object with only a type property? Well, in this case, no because we just returned the request object we did not return our action and thats why we ended up seeing the nasty red message saying Actions must be plain objects. So we did not return a plain object and actions must return plain objects. We returned a request object that probably has some fancy methods assigned to it and probably not a type property, so we definitely did not dispatch what we thought we were dispatching.

This is all because of the async/await syntax that you are using.

So that is issue number 1 with your action creator. As a result of using the async/await syntax which gets transpiled down to es5 code, what actually runs inside your browser is not what you think actually runs.

So we are dispatching a NOT Redux action, we are dispatching a random object that Redux does not care about.

So how do we properly make use of this middleware called Redux-Thunk? Before we answer that, let's understand what a middleware is in the world of Redux.

A middleware is a plain JavaScript function that will be called with every single action that we dispatch. Inside that function, a middleware has the opportunity to stop an action from being dispatched, prevent it from going to any reducers, modify an action or manipulate an action in any way, shape or form.

Redux-Thunk is the most popular middleware, because it helps us work with asynchronous action creators.

Okay, so how does Redux-Thunk help us solve this problem?

Well, Redux-Thunk will relax the normal action creator rules or Redux which says, as I have been saying above, that an action creator must return action objects, it must have a type property and optionally, a payload property.

There is nothing intrinsic about Redux-Thunk, it allows us to do many things, one of them being handling action creators, but its not its primary purpose.

Once we have Redux-Thunk involved in our action creator it can return plain objects OR it can return functions.

You see where this is going?

So how does returning a function help?

So our action creator returns an "action" in the form of an object or function. That "action" will be sent to the dispatch function and eventually it will end up inside of Redux-Thunk.

Redux-Thunk will say, "hi action, are you a function or are you an object?" If the "action" tells Redux-Thunk its an object, Redux-Thunk will say, "well, thanks for stopping by, action, but I prefer to only deal with functions" and then Redux-Thunk will shove "action" towards the reducers.

Otherwise, Redux-Thunk will say, "oh so you are a function? Nice!" Redux-Thunk will then invoke your function and it passes the dispatch, getState functions as arguments. You were already given the syntax version of your answer, so allow me to offer a variation of it.

So instead of just this:

    export const fetchPosts = async () => {
      const response  = await jsonPlaceholder.get('/posts');
      return {
        type: 'FETCH_POSTS',
        payload: response
      }
    };

with Redux-Thunk you would include this:

    export const fetchPosts = async () => {
      return function(dispatch, getState) {
        const response  = await jsonPlaceholder.get('/posts');
        return {
          type: 'FETCH_POSTS',
          payload: response
        }
      }
    };

Now in the above example I am making an asynchronous request with my action creator to an outside API. So this dispatch has unlimited powers to change the data on the Redux side of our application.

You see me utilizing getState so you can also understand that in addition to dispatch, getState will return all of the data inside of your store. These two arguments have unlimited power inside our Redux application. Through dispatch we can change any data we want and through getState we can read any data we want.

Go to the source code of Redux-Thunk itself: https://github.com/reduxjs/redux-thunk/blob/master/src/index.js

The above is all of Redux-Thunk. Only 6 to 7 lines do anything, the others are initialization steps, function declarations and export. On line 2 is a series of functions that return functions.

In the body of it, you see the logic of whats going on and it asks, did you dispatch and action and if so is it an action or a function?

Everything I described above is captured in the source code.

So for me to properly apply Redux-Thunk to the example I gave you I would go to my root index.js file and import after installing it in terminal like so:

    import React from "react";
    import ReactDOM from "react-dom";
    import { Provider } from "react-redux";
    import { createStore, applyMiddleware } from "redux";
    import thunk from 'redux-thunk';
    
    import App from "./components/App";
    import reducers from "./reducers";
    
    ReactDOM.render(
      <Provider store={createStore(reducers)}>
        <App />
      </Provider>,
      document.querySelector("#root")
    );

Notice I also imported the applyMiddleware. This function is how we connect a middleware to Redux.

So then I apply the createStore up front into a variable called store and implement that inside the Provider store like so:

    const store = createStore(reducers);
    
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.querySelector("#root")
    );

To hook up Redux-Thunk, as a second argument I will call applyMiddleware and pass in thunk like so:

    const store = createStore(reducers, applyMiddleware(thunk));
    
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.querySelector("#root")
    );

Then inside my action creator I make one or two changes. I can still return an normal object with a type property, that is an option, with Redux-Thunk we can still make normal action creators that return objects, but we don't have to return an action.

So rather than returning an action I can call dispatch and pass in my action object like so:

    export const fetchPosts = () => {
      return async function(dispatch, getState) {
        const response  = await jsonPlaceholder.get('/posts');
        
        dispatch({type: 'FETCH_POSTS', payload: response })
      }
    };

With Redux-Thunk we can use async/await syntax, because this syntax is only going to modify the return value of the inner function. Nothing from the function will ever get used. Redux-Thunk will not get a reference of what gets returned and make use of it, we can return or not return, its what we return from our outer function is what we care about.

A common way of refactoring what I just shared above is like so:

    export const fetchPosts = () => {
      return async (dispatch) => {
        const response  = await jsonPlaceholder.get('/posts');
    
        dispatch({type: 'FETCH_POSTS', payload: })
      }
    };

So if you don't make use of getState within the function, you can leave it out as an argument. You can make your code even more concise like so:

    export const fetchPosts = () => async dispatch => {
        const response  = await jsonPlaceholder.get('/posts');
        
        dispatch({type: 'FETCH_POSTS', payload: response })
    } 

You will see this in a lot of Redux projects. Thats it.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...