async / await in TypeScript

"use strict";
// printDelayed is a 'Promise<void>'
async function printDelayed(elements: string[]) {
  for (const element of elements) {
    await delay(400);
    console.log(element);
  }
}
async function delay(milliseconds: number) {
  return new Promise<void>((resolve) => {
    setTimeout(resolve, milliseconds);
  });
}
printDelayed(["Hello", "beautiful", "asynchronous", "world"]).then(() => {
  console.log();
  console.log("Printed every element!");
});

References
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-7.html

Writing Thunks using createAsyncThunk in React Redux

We can write thunks that dispatch “loading”, “request succeeded”, and “request failed” actions. We had to write action creators, action types, and reducers to handle those cases.

Because this pattern is so common, Redux Toolkit has a createAsyncThunk API that will generate these thunks for us. It also generates the action types and action creators for those different request status actions, and dispatches those actions automatically based on the resulting Promise.

booksSlices.ts

import {createAsyncThunk, createEntityAdapter, createSlice} from "@reduxjs/toolkit";
import {RootState} from "../../app/store";

export type BookState = { bookId: number, bookTitle: string, };

export const addOneBookAsync = createAsyncThunk(
    "books/addOneBookAsync",
    async (bookTitle: string) => {
        let result;
        setTimeout(result = function () {
            const newBook: BookState = {bookId: Math.floor(Math.random() * 1000), bookTitle: bookTitle};
            return newBook;
        }, 1000);
        return result();
    }
);

const booksAdapter = createEntityAdapter<BookState>({
    selectId: model => model.bookId,
    sortComparer: (a, b) => a.bookTitle.localeCompare(b.bookTitle),
});

export const booksSlice = createSlice({
    name: "books",
    initialState: booksAdapter.getInitialState,
    reducers: {
        addOne: booksAdapter.addOne,
    },
    extraReducers: builder => {
        builder
            .addCase(addOneBookAsync.pending, state => {
                // we can update state while loading data
                console.log("loading");
            })
            .addCase(addOneBookAsync.fulfilled, (state, action) => {
                // we can update state after successful response or we can call another reducer
                // here we are going to call another reducer
                booksSlice.caseReducers.addOne(state, action);
                console.log("successful");
            }).addCase(addOneBookAsync.rejected, state => {
            // we can update state after failed
            console.log("failed");
        })
    }
});

export const booksActions = booksSlice.actions;
export const booksSelectors = booksAdapter.getSelectors<RootState>(
    (state) => state.books
)

export default booksSlice.reducer;

Books.tsx

import {useAppSelector, useAppDispatch} from '../../app/hooks';
import {BookState, booksActions, booksSelectors, addOneBookAsync} from "./booksSlice";
import {useState} from "react";

function Books() {

    const dispatch = useAppDispatch();
    const books = useAppSelector(booksSelectors.selectAll);
    const [bookTitle, setBookTitle] = useState("");

    const addHandler = () => {
        const newBook: BookState = {
            bookId: Math.floor(Math.random() * 1000),
            bookTitle: bookTitle
        };
        dispatch(booksActions.addOne(newBook));
    }

    const addAsyncHandler = async () => {
        try {
            const result = await dispatch(addOneBookAsync(bookTitle)).unwrap();
            console.log(result);
        } catch (rejectedValueOrSerializedError) {
            // handle error here
        }
    }

    return (
        <div>

            <div className="mx-2 my-2">
                <input type="text" placeholder="Books Title" className="input input-bordered w-full max-w-xs"
                       value={bookTitle} onChange={event => setBookTitle(event.target.value)}/>
                <button className="btn mx-2" onClick={addHandler}>Add</button>
                <button className="btn mx-2" onClick={addAsyncHandler}>Add Async</button>
            </div>

            <ul>
                {books.map((value, index) => (
                    <li key={value.bookId}>Title : {value.bookTitle}, Id : {value.bookId}</li>
                ))}
            </ul>
        </div>
    );
}

export default Books;

Return Value

createAsyncThunk returns a standard Redux thunk action creator. The thunk action creator function will have plain action creators for the pendingfulfilled, and rejected cases attached as nested fields.

Handling Thunk Results​

The promise returned by the dispatched thunk has an unwrap property which can be called to extract the payload of a fulfilled action or to throw either the error or, if available, payload created by rejectWithValue from a rejected action:

const onClick = () => {
  dispatch(fetchUserById(userId))
    .unwrap()
    .then((originalPromiseResult) => {
      // handle result here
    })
    .catch((rejectedValueOrSerializedError) => {
      // handle error here
    })
}

Or with async/await syntax:

const onClick = async () => {
  try {
    const originalPromiseResult = await dispatch(fetchUserById(userId)).unwrap()
    // handle result here
  } catch (rejectedValueOrSerializedError) {
    // handle error here
  }
}

References
https://redux-toolkit.js.org/api/createAsyncThunk
https://redux.js.org/tutorials/fundamentals/part-8-modern-redux#writing-thunks
https://redux-toolkit.js.org/api/createslice#extrareducers
https://stackoverflow.com/questions/65106681/redux-toolkit-dispatch-an-action-from-an-extrareducers-listener

Normalizing State with createEntityAdapter in React Redux

We can read how to “normalize” state from here, but simply we can do it  by keeping items in an object keyed by item IDs. This gives us the ability to look up any item by its ID without having to loop through an entire array. However, writing the logic to update normalized state by hand was long and tedious. Writing “mutating” update code with Immer makes that simpler, but there’s still likely to be a lot of repetition – we might be loading many different types of items in our app, and we’d have to repeat the same reducer logic each time.

Redux Toolkit includes a createEntityAdapter API that has prebuilt reducers for typical data update operations with normalized state. This includes adding, updating, and removing items from a slice. createEntityAdapter also generates some memoized selectors for reading values from the store.

createEntityAdapter is a function that generates a set of prebuilt reducers and selectors for performing CRUD operations on a normalized state structure containing instances of a particular type of data object. These reducer functions may be passed as case reducers to createReducer and createSlice. They may also be used as “mutating” helper functions inside of createReducer and createSlice.

booksSlice.ts

import {createEntityAdapter, createSlice} from "@reduxjs/toolkit";
import {RootState} from "../../app/store";

export type BookState = { bookId: number, bookTitle: string, };

const booksAdapter = createEntityAdapter<BookState>({
    selectId: model => model.bookId,
    sortComparer: (a, b) => a.bookTitle.localeCompare(b.bookTitle),
});

export const booksSlice = createSlice({
    name: "books",
    initialState: booksAdapter.getInitialState,
    reducers: {
        addOne: booksAdapter.addOne,
    }
});

export const booksActions = booksSlice.actions;
export const booksSelectors = booksAdapter.getSelectors<RootState>(
    (state) => state.books
)

export default booksSlice.reducer;

Books.tsx

import {useAppSelector, useAppDispatch} from '../../app/hooks';
import {BookState, booksActions, booksSelectors} from "./booksSlice";
import {useState} from "react";

function Books() {

    const dispatch = useAppDispatch();
    const books = useAppSelector(booksSelectors.selectAll);
    const [bookTitle, setBookTitle] = useState("");

    const addHandler = () => {
        const newBook: BookState = {
            bookId: Math.floor(Math.random() * 1000),
            bookTitle: bookTitle
        };
        dispatch(booksActions.addOne(newBook));
    }

    return (
        <div>

            <div className="mx-2 my-2">
                <input type="text" placeholder="Books Title" className="input input-bordered w-full max-w-xs"
                       value={bookTitle} onChange={event => setBookTitle(event.target.value)}/>
                <button className="btn mx-2" onClick={addHandler}>Add</button>
            </div>

            <ul>
                {books.map((value, index) => (
                    <li key={value.bookId}>Title : {value.bookTitle}, Id : {value.bookId}</li>
                ))}
            </ul>
        </div>
    );
}

export default Books;

References
https://redux-toolkit.js.org/api/createEntityAdapter
https://redux.js.org/tutorials/fundamentals/part-8-modern-redux#normalizing-state
https://redux.js.org/usage/structuring-reducers/normalizing-state-shape

Implementing Counter Example in Typescript React using Redux Toolkit

store.ts

import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

hooks.ts

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

counterApi.ts

// A mock function to mimic making an async request for data
export function fetchCount(amount = 1) {
  return new Promise<{ data: number }>((resolve) =>
    setTimeout(() => resolve({ data: amount }), 500)
  );
}

counterSlice.ts

import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState, AppThunk } from '../../app/store';
import { fetchCount } from './counterAPI';

export interface CounterState {
  value: number;
  status: 'idle' | 'loading' | 'failed';
}

const initialState: CounterState = {
  value: 0,
  status: 'idle',
};

// 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. Thunks are
// typically used to make async requests.
export const incrementAsync = createAsyncThunk(
  'counter/fetchCount',
  async (amount: number) => {
    const response = await fetchCount(amount);
    // The value we return becomes the `fulfilled` action payload
    return response.data;
  }
);

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  // The `reducers` field lets us define reducers and generate associated actions
  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;
    },
  },
  // The `extraReducers` field lets the slice handle actions defined elsewhere,
  // including actions generated by createAsyncThunk or in other slices.
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.status = 'idle';
        state.value += action.payload;
      })
      .addCase(incrementAsync.rejected, (state) => {
        state.status = 'failed';
      });
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
export const selectCount = (state: RootState) => state.counter.value;

// We can also write thunks by hand, which may contain both sync and async logic.
// Here's an example of conditionally dispatching actions based on current state.
export const incrementIfOdd =
  (amount: number): AppThunk =>
  (dispatch, getState) => {
    const currentValue = selectCount(getState());
    if (currentValue % 2 === 1) {
      dispatch(incrementByAmount(amount));
    }
  };

export default counterSlice.reducer;

Counter.tsx

import React, { useState } from 'react';

import { useAppSelector, useAppDispatch } from '../../app/hooks';
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  incrementIfOdd,
  selectCount,
} from './counterSlice';
import styles from './Counter.module.css';

export function Counter() {
  const count = useAppSelector(selectCount);
  const dispatch = useAppDispatch();
  const [incrementAmount, setIncrementAmount] = useState('2');

  const incrementValue = Number(incrementAmount) || 0;

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
      </div>
      <div className={styles.row}>
        <input
          className={styles.textbox}
          aria-label="Set increment amount"
          value={incrementAmount}
          onChange={(e) => setIncrementAmount(e.target.value)}
        />
        <button
          className={styles.button}
          onClick={() => dispatch(incrementByAmount(incrementValue))}
        >
          Add Amount
        </button>
        <button
          className={styles.asyncButton}
          onClick={() => dispatch(incrementAsync(incrementValue))}
        >
          Add Async
        </button>
        <button
          className={styles.button}
          onClick={() => dispatch(incrementIfOdd(incrementValue))}
        >
          Add If Odd
        </button>
      </div>
    </div>
  );
}

 

Redux Fundamentals, Part 8: Modern Redux with Redux Toolkit

  • Redux Toolkit (RTK) is the standard way to write Redux logic
    • RTK includes APIs that simplify most Redux code
    • RTK wraps around the Redux core, and includes other useful packages
  • configureStore sets up a Redux store with good defaults
    • Automatically combines slice reducers to create the root reducer
    • Automatically sets up the Redux DevTools Extension and debugging middleware
  • createSlice simplifies writing Redux actions and reducers
    • Automatically generates action creators based on slice/reducer names
    • Reducers can “mutate” state inside createSlice using Immer
  • createAsyncThunk generates thunks for async calls
    • Automatically generates a thunk + pending/fulfilled/rejected action creators
    • Dispatching the thunk runs your payload creator and dispatches the actions
    • Thunk actions can be handled in createSlice.extraReducers
  • createEntityAdapter provides reducers + selectors for normalized state
    • Includes reducer functions for common tasks like adding/updating/removing items
    • Generates memoized selectors for selectAll and selectById

References
https://redux.js.org/tutorials/fundamentals/part-8-modern-redux

Redux Fundamentals, Part 7: Standard Redux Patterns

  • Action creator functions encapsulate preparing action objects and thunks
    • Action creators can accept arguments and contain setup logic, and return the final action object or thunk function
  • Memoized selectors help improve Redux app performance
    • Reselect has a createSelector API that generates memoized selectors
    • Memoized selectors return the same result reference if given the same inputs
  • Request status should be stored as an enum, not booleans
    • Using enums like 'idle' and 'loading' helps track status consistently
  • “Flux Standard Actions” are the common convention for organizing action objects
    • Actions use payload for data, meta for extra descriptions, and error for errors
  • Normalized state makes it easier to find items by ID
    • Normalized data is stored in objects instead of arrays, with item IDs as keys
  • Thunks can return promises from dispatch
    • Components can wait for async thunks to complete, then do more work

References
https://redux.js.org/tutorials/fundamentals/part-7-standard-patterns

Redux Fundamentals, Part 6: Async Logic and Data Fetching

  • Redux middleware were designed to enable writing logic that has side effects
    • “Side effects” are code that changes state/behavior outside a function, like AJAX calls, modifying function arguments, or generating random values
  • Middleware add an extra step to the standard Redux data flow
    • Middleware can intercept other values passed to dispatch
    • Middleware have access to dispatch and getState, so they can dispatch more actions as part of async logic
  • The Redux “Thunk” middleware lets us pass functions to dispatch
    • “Thunk” functions let us write async logic ahead of time, without knowing what Redux store is being used
    • A Redux thunk function receives dispatch and getState as arguments, and can dispatch actions like “this data was received from an API response”

References
https://redux.js.org/tutorials/fundamentals/part-6-async-logic

Redux Fundamentals, Part 5: UI and React

  • Redux stores can be used with any UI layer
    • UI code always subscribes to the store, gets the latest state, and redraws itself
  • React-Redux is the official Redux UI bindings library for React
    • React-Redux is installed as a separate react-redux package
  • The useSelector hook lets React components read data from the store
    • Selector functions take the entire store state as an argument, and return a value based on that state
    • useSelector calls its selector function and returns the result from the selector
    • useSelector subscribes to the store, and re-runs the selector each time an action is dispatched.
    • Whenever the selector result changes, useSelector forces the component to re-render with the new data
  • The useDispatch hook lets React components dispatch actions to the store
    • useDispatch returns the actual store.dispatch function
    • You can call dispatch(action) as needed inside your components
  • The <Provider> component makes the store available to other React components
    • Render <Provider store={store}> around your entire <App>

References
https://redux.js.org/tutorials/fundamentals/part-5-ui-react