Redux

Selectors

/* do this */
const foo = useAppSelector((state) => state.foo)
const baz = useAppSelector((state) => state.bar.baz)

/* don't do this - this will re-render when anything in the state changes */
const {
    foo,
    bar: { baz }
} = useAppSelector((state) => state)

/* don't do this - this will re-render every time an action is dispatched, since the returned object isn't referentially the same */
const { foo, baz } = useAppSelector((state) => ({
    foo: state.foo,
    baz: state.bar.baz
}))

Memoized selectors

import { createSelector } from '@reduxjs/toolkit'

export const selectTodoIds = createSelector(
  // First, pass one or more "input selector" functions:
  state => state.todos,
  // Then, an "output selector" that receives all the input results as arguments
  // and returns a final result value
  todos => todos.map(todo => todo.id)
)

Actions

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}
const todoAdded = text => {
  return {
    type: 'todos/todoAdded',
    payload: text
  }
}

store.dispatch(todoAdded('Buy milk'))

Reducers

Dispatch

store.dispatch({ type: 'counter/incremented' })

Thunks (async dispatch)

// fetchTodoById is the "thunk action creator"
export function fetchTodoById(todoId) {
  // fetchTodoByIdThunk is the "thunk function"
  return async function fetchTodoByIdThunk(dispatch, getState) {
    const response = await client.get(`/fakeApi/todo/${todoId}`)
    dispatch(todosLoaded(response.todos))
  }
}

store.dispatch(fetchTodoById(123))

createSlice

import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'

interface CounterState {
  value: number
}

const initialState = { value: 0 } satisfies CounterState as CounterState

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment(state) {
      state.value++
    },
    decrement(state) {
      state.value--
    },
    incrementByAmount(state, action: PayloadAction<number>) {
      state.value += action.payload
    },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
// error handling omitted
reducers: (create) => {
    increment: create.reducer((state) => state.value++),
    fetchValue: create.asyncThunk(async (payload) => {
        return {
            response: await fetch('/value', {
                method: 'POST',
                body: payload.body
            }).then((response) => response.json())
        }
    }, {
        pending: (state) => { /* ... */ },
        fulfilled: (state, action) => {
            state.value = action.payload.response
        },
        rejected: (state, action) => { /* ... */ },
        settled: (state, action) => { /* ... */ },
    })
}
import { buildCreateSlice, asyncThunkCreator } from '@reduxjs/toolkit'

export const createAppSlice = buildCreateSlice({
  creators: { asyncThunk: asyncThunkCreator },
})