Awaiting Redux Action w/ React Hooks

Viewed 2896

I'm trying to handle a form submission to show a loading component when the data fetch is occuring. I'd like to display the data when it's been loaded into my Redux store.

Right now, I've set up my component to use React hooks. While the data loads into my redux store successfully, I'm not sure how to "await" the result of the action being completed. Here's a simplified version of what my component looks like:

const DataPage = (props) => {

    const [isLoading, setIsLoading] = useState(false);
    const [isError, setError] = useState(false);

    useEffect(() => { // Reset Filters when dataSource changes...
        setError(false);
        setIsLoading(false);
    }, [dataSource]);

    const handleSubmit = (e, { dataSource }) => {

        e.preventDefault();
        setError(false)
        setIsLoading(true);

         //// IDEALLY THIS IS WHERE THE FIX WOULD GO? TURN THIS INTO ASYNC/AWAIT?
        props.fetchData({ dataSource, token: localStorage.JWT_TOKEN });
    };

    return (    
        <div className="dataPage">
            <form className="dataPage__filters" onSubmit={(e) => handleSubmit(e, { dataSource })}>
                <DataSelector dataSource={dataSource} setDataSource={setDataSource}/>
                <button className="button">
                   Search
                </button>
            </form>
            {isError && <div>Something went wrong...</div>}
            { isLoading ? ( <div>...Loading </div> ) : (
                <div className="dataPage__table">
                    <DataTable /> // This is connected to my redux-store separately through 'connect'
                </div>
            )}
        </div>
    );
};

const mapDispatchToProps = (dispatch) => ({
    fetchData: ({ dataSource, token }) => dispatch(startFetchData({ dataSource, token }))
});

export default connect(null, mapDispatchToProps)(DataPage);

The relevant actions (startFetchData, and setData) are located in another file, and look like this:

export const setData = (data) => ({
    type: "SET_DATA",
    data
});

export const startFetchData = ({ dataSource, filter, filterTarget, token }) => {
    return (dispatch) => {

        axios.get(`${'http://localhost:8081'}/api/${dataSource}`, { headers: { authorization: token }})
        .then((res) => {
            dispatch(setData(result));
        });
    }
};

I'd like to be able to do this without introducing any new dependencies if possible.

2 Answers

A note for those using TypeScript: If you want to await a promise returned by an action using useDispatch() you may see TypeScript complaining about an unnecessary await.

In this case make sure to add the correct typing (see ThunkDispatch) to useDispatch via generics.

Also with useEffect() with async-await syntax make sure to wrap your async code in another closure because useEffect() expects a void return value and Typescript otherwise complains about you returning a Promise.

const dispatch = useDispatch<ThunkDispatch<any, any, Action>>();

useEffect(() => {
  (async () => {
    const myResult = await dispatch(...);
    const anotherResult = await dispatch(...);
    // ...
  })();
});

I recommend you to use redux-thunk middleware. It's really easy and useful library to able your action to be, instead of objects, functions (including async functions). I'll give you an example:

Store.js

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
import api from './services/api';

// Note: this API requires redux@>=3.1.0
const store = createStore(
  rootReducer,
  // With extra argument, in this case, my API):
  applyMiddleware(thunk.withExtraArgument(api));
);


AuthDuck.js

Giving this duck (types, actions and reducers in the same file, see more here)

// ----------------------------------------------------------------
// Types
// ----------------------------------------------------------------
const Types = {
  SIGN_IN_START: 'SIGN_IN_START',
  SIGN_IN_SUCCESS: 'SIGN_IN_SUCCESS',
  SIGN_IN_FAIL: 'SIGN_IN_FAIL'
};

// ----------------------------------------------------------------
// Actions
// ----------------------------------------------------------------
const signin = function (user) {
  // LOOK HERE!
  // Redux Thunk able you to return a function instead of an object.
  return async function (dispatch, getState, api) {
    try {
      dispatch({ type: Types.SIGN_IN_START });
      const token = await api.access.signin(user);
      dispatch({ type: Types.SIGN_IN_SUCCESS, payload: token });
    } catch (error) {
      dispatch({ type: Types.SIGN_IN_FAIL, payload: error });
    }
  };
};

export const Actions = { signin };

// ----------------------------------------------------------------
// Reducers
// ----------------------------------------------------------------
export default function reducer(state, action) {
  switch (action.type) {
    case VeasyCalendarTypes.SIGN_IN_START:
      return { ...state, isLoading: true };
    case VeasyCalendarTypes.SIGN_IN_SUCCESS:
      return { ...state, isLoading: false, token: action.payload };
    case VeasyCalendarTypes.SIGN_IN_FAIL:
      return { ...state, isLoading: false, error: action.payload };
    default:
      return state;
  }
};

I hope to helped you, let me know if it worked for your case :)

Best regards

Related