redux-templates icon indicating copy to clipboard operation
redux-templates copied to clipboard

Switch async thunk example to be `createAsyncThunk` ?

Open markerikson opened this issue 4 years ago • 8 comments

It may be worth switching the current thunk snippet to be createAsyncThunk instead. Right now, we have:

// The function below is called a thunk and allows us to perform async logic. It
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
// will call the thunk with the `dispatch` function as the first argument. Async
// code can then be executed and other actions can be dispatched
export const incrementAsync = (amount: number): AppThunk => dispatch => {
  setTimeout(() => {
    dispatch(incrementByAmount(amount));
  }, 1000);
};

I'm debating whether a createAsyncThunk with a fake timer-resolved Promise instead of an API call would be better here? Except they're sorta different use cases, conceptually.

Not sure what's better.

The flip side is that now the slice is having to handle listening to the thunk-generated actions, vs the hand-written thunk using the slice-generated action.

Maybe both? I dunno!

I assume the file is usually getting deleted right away anyway, but we want useful examples in it.

markerikson avatar Mar 30 '21 20:03 markerikson

@markerikson I think there was some dialogue about this here: https://github.com/reduxjs/cra-template-redux/pull/26. That implementation is basically the same as what you're saying, and I think it makes sense (although would recommend having a 'handler' return a result within a random realistic timeout 20-400ms).

Is there any interest in having a more 'realistic' template where a lib like msw is used? Even though I'm a big fan of that, it could be annoying for a user to drop the dep if they didn't want to use it for testing or dev envs. I suppose if we did opt into something like that, we could provide a purge script that dropped those deps. Also, not sure what's better :)

msutkowski avatar Mar 30 '21 20:03 msutkowski

Hah, look, I said basically the same thing a year ago :)

I'd rather not go any farther than a counter. Per my comment above, I assume that most folks are either going to look at the counter code a bit to learn from it and then delete it, or just delete it immediately. I want to optimize for removability here.

But, it is worth having a couple decent examples in this file nonetheless.

Uh... lemme come back to this question a little later this evening when I'm not supposed to be doing day job stuff )

markerikson avatar Mar 30 '21 20:03 markerikson

As an example, there are some counters in the RTK Query examples for consideration that leverage msw.

  • handlers: https://github.com/rtk-incubator/rtk-query/blob/next/examples/react/src/mocks/handlers.ts#L35-L51
  • comp: https://github.com/rtk-incubator/rtk-query/blob/next/examples/react/src/features/counter/Counter.tsx

I also agree we should keep it simple. Just gonna leave the above for posterity assuming we need another RTK Query powered template in the future :)

msutkowski avatar Mar 30 '21 20:03 msutkowski

For reference, here's what @rahsheen put together in his RN example project:

https://github.com/rahsheen/example-redux-toolkit-react-native-app/blob/2e578b88a1fd373f7db165dcda97279aa4edcffc/src/features/counter/counterSlice.ts


// The function below is called a thunk and allows us to perform async logic. It
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
// will call the thunk with the `dispatch` function as the first argument. Async
// code can then be executed and other actions can be dispatched
export const incrementAsync = createAsyncThunk(
  'counter/fetchCount',
  async (amount: number, {dispatch}) => {
    // We are faking it a bit here by `await`ing a Promise and using a `setTimeout`.
    // Usually, you would do something more like:
    // ```
    // const response = await userAPI.fetchById(userId)
    // return response.data
    // ```
    await new Promise((resolve) =>
      setTimeout(() => resolve(dispatch(incrementByAmount(amount))), 1000),
    );
  },
);

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    // Use the PayloadAction type to declare the contents of `action.payload`
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(incrementAsync.fulfilled, (state) => {
        state.status = 'idle';
      });
  },
});

which, actually, now that I look at it is a bit of an abuse of createAsyncThunk. Semantically, it oughta just resolve the promise with a number and return the result, letting the thunk auto-dispatch.

markerikson avatar Mar 30 '21 22:03 markerikson

Definitely don't think adding a lib is the way to go, but agree the example should be a bit more realistic as far as resolving a promise with an actual value. That's easily mocked out and easily deleted as well.

rahsheen avatar Mar 30 '21 23:03 rahsheen

I went ahead and updated the example in the template to be more in line with the docs. counterSlice.ts

I'm sure there is some cleanup that can be done, but I think this is more of a realistic example.

rahsheen avatar Mar 31 '21 02:03 rahsheen

FWIW RTK Query is merged back into RTK

nickserv avatar Apr 25 '21 05:04 nickserv

What about making a request to https://httpbin.org/

export const incrementAsync = createAsyncThunk('counter/fetchCount', async (amount: number) => {
  const response = await fetch('https://httpbin.org/post', {
    method: 'POST',
    headers: {
      'content-type': 'application/json'
    },
    body: JSON.stringify({ amount })
  });
  const result: Record<string, any> = await response.json();
  
  return Number(result.json.amount);
});

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    // ... other reducer actions
  },
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.value += action.payload;
        state.status = 'loading';
      })
      .addCase(incrementAsync.fulfilled, (state) => {
        state.status = 'idle';
      });
  },

leosuncin avatar Jun 15 '22 03:06 leosuncin

We did this!

markerikson avatar Apr 30 '23 15:04 markerikson