Version
redux
v4.2.1react-redux
v8.1.3@reduxjs/toolkit
v1.9.7
Sources:
Represents the current data stored in the application.
Redux uses a single centralized state object to manage and store the data for your entire application. This makes it easy to access and modify the data consistently across different parts of your app.
An action is a plain JavaScript object that describes an event or something that has happened in your application. It typically has a type
property that indicates the type of action being performed and may also carry additional data (payload) relevant to the action.
Actions are used to trigger changes to the application's state. When you want to update the state, you dispatch an action. (Then, Reducers handle these actions to determine how the state should change.)
Example of an hardcoded action:
const incrementByThree = {type: 'counter/increment', payload: 3}
However, this is not very useful since we have to create the object above every time we need to call it. So, we can turn this into a "reusable action" that takes the payload as an argument:
const increment = (amount: number) => ({
type: 'counter/increment',
payload: amount,
});
const action = increment(3)
// { type: 'counter/increment', payload: 3 }
example in TMUI code at src/store/pages/contentManagement/creatives/actions.js
createSlice
. However, we can also create an action separately:
import { createAction } from '@reduxjs/toolkit'
const increment = createAction<number | undefined>('counter/increment')
let action = increment()
// { type: 'counter/increment' }
action = increment(3)
// returns { type: 'counter/increment', payload: 3 }
A reducer is a function that specifies how the application's state should change in response to an action. It takes the current state and an action as arguments and returns a new state object based on those inputs.
Reducers are at the core of Redux. They define the logic for updating the state in a predictable and immutable way. Reducers are typically pure functions, meaning they don't modify the current state directly but return a new state object.
// Use the initialState as a default value
export default function counterReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
// Do something here based on the different types of actions
case 'counter/increment': {
// We need to return a new state object
return {
// that has all the existing state data
...state,
// but has a new number for the `value` field
value: state.value + 1
}
}
default:
// If this reducer doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}
example in TMUI code at src/store/pages/contentManagement/creatives/reducer.js
docs of createReducer()
import { createAction, createReducer } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
const increment = createAction('counter/increment')
const initialState = { value: 0 } as CounterState
const counterReducer = createReducer(initialState, (builder) => {
builder
.addCase(increment, (state, action) => {
state.value++
})
})
docs of createSlice()
A function that accepts an initial state, an object of reducer functions, and a "slice name", and automatically generates action creators and action types that correspond to the reducers and state.
import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
const initialState = { value: 0 } 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
The store is like a big container that holds all the data (state) for your entire application.
It's the heart of Redux. The store is responsible for keeping track of your application's data and provides a way to access and update that data in a controlled and predictable manner. It ensures that your data is consistent and can be easily shared among different parts of your app.
- The store is created by passing in a reducer,
- the store has a method called
getState
that returns the current state value
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
- Selectors are functions that know how to extract specific pieces of information from a store state value.
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
- The only way to update the state is to call
store.dispatch()
and pass in an action object - The store will run its reducer function and save the new state value inside.
store.dispatch({ type: 'counter/incremented' })
console.log(store.getState())
// {value: 1}
- You can think of dispatching actions as "triggering an event" in the application. Something happened, and we want the store to know about it. Reducers act like event listeners, and when they hear an action they are interested in, they update the state in response.
Redux Toolkit's configureStore
simplifies the setup process, by doing all the work for you. One call to configureStore
will:
- Call
combineReducers
to combine your slices reducers into the root reducer function - Add the thunk middleware and called
applyMiddleware
- In development, automatically add more middleware to check for common mistakes like accidentally mutating the state
- Automatically set up the Redux DevTools Extension connection
- Call
createStore
to create a Redux store using that root reducer and those configuration options
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'
const store = configureStore({ reducer: rootReducer })
// The store now has redux-thunk added and the Redux DevTools Extension is turned on
- The
createSelector
utility from the Reselect library, re-exported for ease of use. - Reselect's
createSelector
creates memoized selector functions that only recalculate the output if the inputs change.
import { createSelector } from 'reselect'
const selectValue = state => state.value;
const selectValuePlusOne = createSelector(
[selectValue],
(valueabc) => { // `valueabc` is the first arguments return value, i.e., `state.value`
return valueabc + 1;
}
)
const exampleState = { value: 5 };
console.log(selectValuePlusOne(exampleState)); // 6
- It's used to read data from the store within a component and trigger re-renders when that data changes.
const selectValue = (state: RootState) => state.value
const value = useSelector(selectValue)
// 0
Q: Why do we not use just selectCounter
and wrap it with useSelector
?
A:
useSelector
subscribes to the store, and re-runs the selector each time an action is dispatched.- When an action is dispatched,
useSelector()
will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render.- Q: Doesn't this mean
useSelector
memoises? - A: Yes and no. The value might be the same, but if its a new reference, the component will still rerender. If we don't want to rerender even if its a different reference, then we use
useSelector
+createSelector
like the following:
- Q: Doesn't this mean
import React from 'react'
import { useSelector } from 'react-redux'
import { createSelector } from 'reselect'
const selectNumCompletedTodos = createSelector(
(state) => state.todos,
(todos) => todos.filter((todo) => todo.completed).length
)
export const CompletedTodosCounter = () => {
const numCompletedTodos = useSelector(selectNumCompletedTodos)
return <div>{numCompletedTodos}</div>
}
export const App = () => {
return (
<>
<span>Number of completed todos:</span>
<CompletedTodosCounter />
</>
)
}
- dispatches actions
import React from 'react'
import { useDispatch } from 'react-redux'
import type { Dispatch } from 'redux'
export const CounterComponent = ({ value }) => {
const dispatch: Dispatch = useDispatch()
return (
<div>
<span>{value}</span>
<button onClick={() => dispatch({ type: 'counter/incremented' })}>
Increment counter
</button>
</div>
)
}
or
// 📂 store/counter/reducer.ts
import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
const initialState = { value: 0 } 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
// 📂 components/someFile.tsx
import React from 'react'
import type { Dispatch } from 'redux'
import { useDispatch } from 'react-redux'
import { incrementByAmount } from 'store/counter/reducer';
export const CounterComponent = ({ value }) => {
const dispatch: Dispatch = useDispatch()
return (
<div>
<span>{value}</span>
<button onClick={() => dispatch(incrementByAmount(5))}>
Increment counter
</button>
</div>
)
}
![[redux-data-flow.gif]]
Historically, a thunk
usually refers to a small piece of code that is called as a function, does some small thing, and then JUMP
s to another location (usually a function) instead of returning to its caller (source). So, a thunk helps us delay the execution/work of some code. I italicised the word usually because it has more meanings, but they are not our concern right now.
- For Redux specifically, a thunk calls
dispatch
andgetState
methods within the body of the thunk function. - Thunk functions are not directly called by application code. Instead, they are passed to
store.dispatch()
:
const thunkFunction = (dispatch, getState) => {
// logic here that can dispatch actions or read state
// may contain _any_ arbitrary logic, sync or async, and can call `dispatch` or `getState` at any time.
}
store.dispatch(thunkFunction)
In the same way that Redux code normally uses action creators to generate action objects for dispatching :
// "action creator" function
const todoAdded = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}
// calling the "action creator" (i.e., todoAdded(...), and then passing the resulting action object directly to dispatch
store.dispatch(todoAdded('Buy milk'))
// whereas the code below is not good practice
dispatch({ type: 'todos/todoAdded', payload: trimmedText })
// why is it bad practice?
// because we have to write that whole thing every single time we want to dispatch that action. It is prone to errors and has lots of duplication
instead of writing action objects by hand, we normally use thunk action creators to generate the thunk functions that are dispatched.
A thunk action creator is a function that may have some arguments, and returns a new thunk function. The thunk typically closes over any arguments passed to the action creator, so they can be used in the logic:
// 📂 someFile.ts
// 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))
}
}
// 📂 todoComponent.tsx
function TodoComponent({ todoId }) {
const dispatch = useDispatch()
const onFetchClicked = () => {
// Calls the thunk action creator, and passes the thunk function to dispatch
dispatch(fetchTodoById(todoId))
}
}
- Thunks allow us to write additional Redux-related logic separate from a UI layer.
- This logic can include side effects, such as async requests or generating random values, as well as logic that requires dispatching multiple actions or access to the Redux store state.
In a sense, a thunk is a loophole where you can write any code that needs to interact with the Redux store, ahead of time, without needing to know which Redux store will be used.
some use cases:
- Moving complex logic out of components
- Making async requests or other async logic
- Writing logic that needs to dispatch multiple actions in a row or over time
- Writing logic that needs access to
getState
to make decisions or include other state values in an action
A function that accepts a Redux action type string and a callback function that should return a promise. It generates promise lifecycle action types based on the action type prefix that you pass in, and returns a thunk action creator that will run the promise callback and dispatch the lifecycle actions based on the returned promise.
- takes
dispatch
andgetState
arguments - dispatches actions within the thunk/function body
const fetchData = () => (dispatch: Dispatch<typeOfPassedParameters>, getState: RootState) => {
dispatch({ type: 'FETCH_DATA_REQUEST' });
// Perform asynchronous logic, e.g., fetch data from an API
fetch('/api/data')
.then(response => response.json())
.then(data => {
dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data }); })
.catch(error => {
dispatch({ type: 'FETCH_DATA_FAILURE', error });
});
};
- utility function provided by RTK
- generates a set of action creators (i.e., pending, fulfilled, rejected)
these thunks fit better when using RTK because t
createAsyncThunk
is fully supported by RTK- can handle more action types (e.g.,
data/fetchData
automatically generate the following actions:data/fetchData/pending
anddata/fetchData/fulfilled
etc.) - can access more state changing functions using the
thunkAPI
argument
import { createAsyncThunk } from '@reduxjs/toolkit';
const dataAPI = {
async function fetch('url') {
// fetch the url
// return response
}
}
const fetchData = createAsyncThunk('data/fetchData', async (_, thunkAPI) => {
// const { dispatch } = thunkAPI;
// dispatch(someAction())
// const currentState = thunkAPI.getState();
const response = await dataAPI.fetch('/api/data');
const data = response.json();
return data;
});
const initialState = {
someData: [],
loading: false,
} as UsersState
// Then, handle actions in your reducers:
const dataSlice = createSlice({
name: 'data',
initialState,
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
// !
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
// type: data/fetchData/pending
[fetchData.pending]: state => {
state.loading = true
},
// type: data/fetchData/fulfilled
[fetchData.fulfilled]: (state, { payload }) => {
state.loading = false;
state.data = payload;
},
// same as above
builder.addCase(fetchData.fulfilled, (state, action) => {
state.loading = false;
state.data = payload;
})
},
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchData())