how to dispatch asyncThunk inside another asyncThunk

Viewed 2105

I've created two async thunks:

export const foo = createAsyncThunk(
    'slice/foo',
    async ( ) => {
        const res = await fetch('/')
        return res.data
    }
);

export const bar = createAsyncThunk(
    'slice/bar',
    async ( ) => {
        const res = await foo()
        return res.data
    }
);

Basically I'm trying to call foo() inside bar() Then in the useEffect of react component, I'm dispatching bar() like this:

React.useEffect( async () => {
    await dispatch( bar() );
}, [bar]);

foo being dispatched but bar not.

What am I doing wrong?

Thank you in advance.

Edit1. it kinda feels like an antipattern, but what I'm basically trying to do is to chain bar() and foo(), and I don't know any other (proper?) way

Edit2. based on @Nadia Chibrikova comment, and this is something I've tried as well using thunkAPI:

    export const bar = createAsyncThunk(
        'slice/bar',
        async ( thunkApi ) => {
            thunkApi .dispatch(foo())
        }
    );

but I'm getting the following error:

Cannot read property 'dispatch' of undefined
1 Answers

Chaining Thunks

This is not a great design pattern but it can be done. Your "Edit2" is close, but thunkApi is the second argument of the payload creator. The first argument is the args that you pass when you call the action creator as bar(args). If you don't need an argument then you can use a _ to denote that this variable is unused.

export const foo = createAsyncThunk(
  "slice/foo", 
  async () => {
    const res = await fetch("/");
    return res.json();
  }
);

export const bar = createAsyncThunk(
  "slice/bar", 
  async (_, { dispatch }) => {
    const fooAction = await dispatch(foo());
    return fooAction.payload;
  }
);

Calling dispatch(bar()) will dispatch the actions in this order:

  1. "slice/bar/pending"
  2. "slice/foo/pending"
  3. "slice/foo/fulfilled"
  4. "slice/bar/fulfilled"

Separate Actions

As recommended by @Marcus Melodious, it is better to have completely separate actions and handle the dispatching in your component. If bar requires some piece of data from foo (like an id or a token), then it should take that data as an argument to its action creator.

Here is an example where we fetch a post and then fetch a user profile based on the userId property of that post.

export const fetchPost = createAsyncThunk(
  "slice/fetchPost",
  async (postId) => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/posts/${postId}`
    );
    return res.json();
  }
);

export const fetchUser = createAsyncThunk(
  "slice/fetchUser",
  async (userId) => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/users/${userId}`
    );
    return res.json();
  }
);

You can use one useEffect and look at the results from the dispatch

useEffect(() => {
  const execute = async () => {
    const postAction = await dispatch(fetchPost(POST_ID));
    const {userId} = postAction.payload;
    dispatch(fetchUser(userId));
  };

  execute();
}, []);

Or you can initiate the two requests from two separate useEffect hooks. Your second hook knows when to dispatch based on changes in the Redux state from useSelector.

const Test = () => {
  const dispatch = useDispatch();
  const post = useSelector((state) => state.slice.post);
  const user = useSelector((state) => state.slice.user);

  const POST_ID = 1; // just a dummy constant for testing

  console.log(post, user);

  useEffect(() => {
    const loadPost = async () => {
      dispatch(fetchPost(POST_ID));
    };
    loadPost();
  }, []); // run once on mount

  useEffect(() => {
    const loadUser = async (userId) => {
      dispatch(fetchUser(userId));
    };
    // check if post has has been set and has a userId
    if ( post && post.userId ) {
      loadUser(post.userId);
    }
  }, [post]); // run when post changes

  return <div/>;
};

Here is a complete code example for the two hooks approach:

import {
  createAsyncThunk,
  configureStore,
  createSlice
} from "@reduxjs/toolkit";
import { Provider, useDispatch, useSelector } from "react-redux";
import React, { useEffect } from "react";

export const fetchPost = createAsyncThunk(
  "slice/fetchPost",
  async (postId) => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/posts/${postId}`
    );
    return res.json();
  }
);

export const fetchUser = createAsyncThunk(
  "slice/fetchUser",
  async (userId) => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/users/${userId}`
    );
    return res.json();
  }
);

const slice = createSlice({
  name: "slice",
  initialState: {
    user: undefined,
    post: undefined
  },
  reducers: {},
  extraReducers: {
    [fetchPost.fulfilled]: (state, action) => {
      state.post = action.payload;
    },
    [fetchUser.fulfilled]: (state, action) => {
      state.user = action.payload;
    }
  }
});

const store = configureStore({ reducer: { slice: slice.reducer } });

const PostHeader = ({ postId }) => {
  const dispatch = useDispatch();
  const post = useSelector((state) => state.slice.post);
  const user = useSelector((state) => state.slice.user);

  console.log(post, user);

  useEffect(() => {
    const loadPost = async () => {
      dispatch(fetchPost(postId));
    };
    loadPost();
  }, [postId, dispatch]); // run once on mount

  useEffect(() => {
    const loadUser = async (userId) => {
      dispatch(fetchUser(userId));
    };
    // check if post has has been set and has a userId
    if (post && post.userId) {
      loadUser(post.userId);
    }
  }, [post, dispatch]); // run when post changes

  return (
    <div>
      {!!post && <h1>{post.title}</h1>}
      {!!user && <h2>By {user.name}</h2>}
    </div>
  );
};

export default function App() {
  return (
    <Provider store={store}>
      <PostHeader postId={1} />
    </Provider>
  );
}
Related