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

Redux Fundamentals, Part 4: Store

  • Redux apps always have a single store
    • Stores are created with the Redux createStore API
    • Every store has a single root reducer function
  • Stores have three main methods
    • getState returns the current state
    • dispatch sends an action to the reducer to update the state
    • subscribe takes a listener callback that runs each time an action is dispatched
  • Store enhancers let us customize the store when it’s created
    • Enhancers wrap the store and can override its methods
    • createStore accepts one enhancer as an argument
    • Multiple enhancers can be merged together using the compose API
  • Middleware are the main way to customize the store
    • Middleware are added using the applyMiddleware enhancer
    • Middleware are written as three nested functions inside each other
    • Middleware run each time an action is dispatched
    • Middleware can have side effects inside
  • The Redux DevTools let you see what’s changed in your app over time
    • The DevTools Extension can be installed in your browser
    • The store needs the DevTools enhancer added, using composeWithDevTools
    • The DevTools show dispatched actions and changes in state over time

References
https://redux.js.org/tutorials/fundamentals/part-4-store

Redux Fundamentals, Part 3: State, Actions, and Reducers

  • Redux apps use plain JS objects, arrays, and primitives as the state values
    • The root state value should be a plain JS object
    • The state should contain the smallest amount of data needed to make the app work
    • Classes, Promises, functions, and other non-plain values should not go in the Redux state
    • Reducers must not create random values like Math.random() or Date.now()
    • It’s okay to have other state values that are not in the Redux store (like local component state) side-by side with Redux
  • Actions are plain objects with a type field that describe what happened
    • The type field should be a readable string, and is usually written as 'feature/eventName'
    • Actions may contain other values, which are typically stored in the action.payload field
    • Actions should have the smallest amount of data needed to describe what happened
  • Reducers are functions that look like (state, action) => newState
    • Reducers must always follow special rules:
      • Only calculate the new state based on the state and action arguments
      • Never mutate the existing state – always return a copy
      • No “side effects” like AJAX calls or async logic
  • Reducers should be split up to make them easier to read
    • Reducers are usually split based on top-level state keys or “slices” of state
    • Reducers are usually written in “slice” files, organized into “feature” folders
    • Reducers can be combined together with the Redux combineReducers function
    • The key names given to combineReducers define the top-level state object keys

References
https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers

Redux Fundamentals, Part 2: Concepts and Data Flow

  • Redux’s intent can be summarized in three principles
    • Global app state is kept in a single store
    • The store state is read-only to the rest of the app
    • Reducer functions are used to update the state in response to actions
  • Redux uses a “one-way data flow” app structure
    • State describes the condition of the app at a point in time, and UI renders based on that state
    • When something happens in the app:
      • The UI dispatches an action
      • The store runs the reducers, and the state is updated based on what occurred
      • The store notifies the UI that the state has changed
    • The UI re-renders based on the new state

References
https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow

Redux Fundamentals, Part 1: Redux Overview

  • Redux is a library for managing global application state
    • Redux is typically used with the React-Redux library for integrating Redux and React together
    • Redux Toolkit is the recommended way to write Redux logic
  • Redux uses several types of code
    • Actions are plain objects with a type field, and describe “what happened” in the app
    • Reducers are functions that calculate a new state value based on previous state + an action
    • A Redux store runs the root reducer whenever an action is dispatched

References
https://redux.js.org/tutorials/fundamentals/part-1-overview

Working with Multiple Slices in Redux Toolkit

import {configureStore, createSlice} from '@reduxjs/toolkit';

const initialCounterState = {counter: 0, showCounter: true};
const initialAuthState = {userName: "", isLoggedIn: false};

const counterSlice = createSlice({
    name: "counter",
    initialState: initialCounterState,
    reducers: {
        increment(state, action) {
            state.counter += action.payload
        },
        decrement(state, action) {
            state.counter -= action.payload
        },
        toggle(state, action) {
            state.showCounter = !state.showCounter
        }
    }
})

const authSlice = createSlice({
    name: "auth",
    initialState: initialAuthState,
    reducers: {
        login(state, action) {
            state.userName = action.payload.userName;
            state.isLoggedIn = true;
        },
        logout(state, action) {
            state.userName = "";
            state.isLoggedIn = false;
        }
    }
});

export const counterActions = counterSlice.actions;
export const authActions = authSlice.actions;

export const store = configureStore({
    reducer: {
        counter: counterSlice.reducer,
        auth: authSlice.reducer,
    }
});

References
https://stackoverflow.com/questions/67577835/same-action-triggering-in-multiple-slices-redux-toolkit