0

Let's say you are using react and redux and you have the following elements on a page:

  • Input field to write something
  • A save button to write your text async to the DB

The expected flow is that the save button should dispatch a save redux thunk to handle the async behaviour of the save. So far, so good.

But there's an extra tricky requirement:

  • Upon save success, the input should be cleared.

I think that there are a couple of ways to handle this:


OPTION 1

  • Add the controlled input state to your Redux store

This is the easiest one. Basically you'll add the input state to your Redux store and when you thunk dispatch the SAVE_SUCCESS action, your reducer will respond with something like this:

SAVE_SUCCESS(state, action) {
  state.inputValue = '';
}

The downside of this is that you'll have to dispatch an action on every key stroke, and that is going to flood your Redux devTools inspector.


OPTION 2

  • Have a controlled input using React local state
  • Pass an clearInput function (using useCallback) to your save thunk

Something like this:

const [inputValue, setInputValue] = useState('initialVale');
const clearInput = useCallback(() => setInputValue(''),[]);

const onClick = dispatch(saveThunk({clearInput});

So, from the thunk you would do something like:

const { clearInput } = thunkProps;
await saveToDB();
clearInput(); // THIS COMES FROM THE thunkProps

OPTION 3

  • Same as option 2 but with an uncontrolled input (so, no state at all)
  • Pass the input ref element to your thunk

So, from the thunk you would do something like:

const { inputRef } = thunkProps;
await saveToDB();
inputRef.current.value = '';

QUESTION

Which one would you go for and why? And also, is there a better approach to this that I'm not aware of?

cbdeveloper
  • 27,898
  • 37
  • 155
  • 336

2 Answers2

1

I would go for awaiting the promise from the thunk in the component, and setting component state there, as shown in our tutorials:

Example:

  const handleKeyDown = async e => {
    // If the user pressed the Enter key:
    const trimmedText = text.trim()
    if (e.which === 13 && trimmedText) {
      // Create and dispatch the thunk function itself
      setStatus('loading')
      // Wait for the promise returned by saveNewTodo
      await dispatch(saveNewTodo(trimmedText))
      // And clear out the text input
      setText('')
      setStatus('idle')
    }
  }
markerikson
  • 63,178
  • 10
  • 141
  • 157
  • Thanks! I usually wrap my thunk code in a `try...catch`, so I can dispatch the `ASYNC_ACTION_FAILURE()` action from the thunk's `catch` block. If I choose to await for the `thunk` to complete from the component, how can I know if the thunk has completed successfully or if it has failed? Should I re-throw from the thunk's catch block? – cbdeveloper Jan 10 '22 at 18:55
  • My main suggestion there would be to use [Redux Toolkit's `createAsyncThunk` API](https://redux-toolkit.js.org/api/createAsyncThunk), which will do that "success/failure" action dispatching for you entirely. You can then do `dispatch(myAsyncThunk()).unwrap()` to convert the result back into a value or thrown error, per that "Essentials" page and https://redux-toolkit.js.org/api/createAsyncThunk#unwrapping-result-actions . – markerikson Jan 10 '22 at 19:01
  • Thanks, again, Mark! Tbh, I've known about the `createAsyncThunk` function for a while now, but I've never used it. What always drove me off from it was the `extraReducers: (builder) => { builder.addCase(someThunk.fulfilled, ... }` pattern. I feel much more in control when I'm creating my own async-lifecycle actions and handling them in the regular case reducers. Ex: `SAVE_START`, `SAVE_SUCCESS` and `SAVE_FAILURE`. Maybe I should start using it, though. Big thanks for improving the documentation. It's been a while since I've read it. I should definitely read it again. – cbdeveloper Jan 10 '22 at 19:21
  • 1
    Yeah, you're really _not_ saving much by hand-defining the actions yourself (even if you're using `createSlice` for those), and `createAsyncThunk` has several additional useful properties around error handling and configuration that are useful anyway. Of course, there's _also_ [our new RTK Query data caching API](https://redux-toolkit.js.org/rtk-query/overview), which can potentially eliminate _all_ thunks and data fetching code from your app if you choose to use it :) – markerikson Jan 10 '22 at 19:41
1

I guess Mark's answer should be the recommended one. But, as another approach (and as a note to myself in the future), if you are not using the createAsyncThunk function to create your thunks, you can specify a value to be returned from a regular thunk, that you create yourself.

This is a very crude example, but would do the trick:

import { AnyAction, ThunkAction } from '@reduxjs/toolkit';

type MaybeSuccess = { success: boolean }
type AppThunkMaybeSuccess = ThunkAction<Promise<MaybeSuccess>, RootState, unknown, AnyAction>;


export const someThunk = (): AppThunkMaybeSuccess => async (dispatch, getState) => {
  try {
    dispatch(SOME_THUNK_START());
    const data = await doSomeAsyncStuff(...);
    dispatch(SOME_THUNK_SUCCESS({ data }));
    return ({ success: true });
  }
  catch(err) {
    const error = err as Error;
    dispatch(SOME_THUNK_FAILURE();
    return ({ success: false });
  }
}

Then, on your component (the one that dispatches the thunk), you could do:

const handleSave = useCallback(async () => {
  const { success } = await dispatch(saveThunk());
  if (success) {
    setInputValue('');
  }
},[]);
cbdeveloper
  • 27,898
  • 37
  • 155
  • 336