Blog>
Snippets

Type-Safe Asynchronous Actions with Redux Thunk

Provide an example of using Redux Thunk with TypeScript to dispatch type-safe asynchronous actions, ensuring that the resolved data conforms to expected types.
interface User {
  id: number;
  name: string;
}

interface RootState {
  users: User[];
}

// Action types
enum ActionTypes {
  FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST',
  FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS',
  FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE'
}

// Action creators
const fetchUsersRequest = () => ({
  type: ActionTypes.FETCH_USERS_REQUEST
});

const fetchUsersSuccess = (users: User[]) => ({
  type: ActionTypes.FETCH_USERS_SUCCESS,
  payload: users
});

const fetchUsersFailure = (error: string) => ({
  type: ActionTypes.FETCH_USERS_FAILURE,
  payload: error
});
Defines the User interface, the RootState, action types, and action creators for fetching users.
import { ThunkAction } from 'redux-thunk';
import { AnyAction } from 'redux';

// Thunk action
export const fetchUsers = (): ThunkAction<void, RootState, unknown, AnyAction> => async (dispatch) => {
  dispatch(fetchUsersRequest());
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const data: User[] = await response.json();
    dispatch(fetchUsersSuccess(data));
  } catch (error) {
    dispatch(fetchUsersFailure(error.message));
  }
};
Creates a Redux Thunk action that fetches users and dispatches appropriate actions based on the request's outcome.
// rootReducer.ts
import { combineReducers } from 'redux';

const usersReducer = (state: User[] = [], action: AnyAction): User[] => {
  switch (action.type) {
    case ActionTypes.FETCH_USERS_SUCCESS:
      return action.payload;
    default:
      return state;
  }
};

const rootReducer = combineReducers({
  users: usersReducer
});

export default rootReducer;
A rootReducer combining all the reducers. Currently including only usersReducer.
// store.ts
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './rootReducer';

const store = createStore(rootReducer, applyMiddleware(thunk));

export default store;
The store file where the Redux store is configured with the thunk middleware.
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
index.tsx is the entry point for React, wrapping the App component with Redux's Provider to make the store available to all components.
/* App.tsx */
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchUsers } from './actions';

const App: React.FC = () => {
  const dispatch = useDispatch();
  const users = useSelector((state: RootState) => state.users);

  useEffect(() => {
    dispatch(fetchUsers());
  }, [dispatch]);

  // Render user list or loading state
  return (
    <ul>{ users.map(user => <li key={user.id}>{user.name}</li>) }</ul>
  );
};

export default App;
App.tsx is the root React component responsible for dispatching the fetchUsers action on mount and rendering the fetched users.