# Redux
redux (opens new window)
redux-toolkit (opens new window)
react-redux (opens new window)
A Global State Management library. It uses "one way data flow" structure
- Predictable - Same behaviour on client, server & native. Easy to test.
- Centralized - State & logic both are Centralized
- Debuggable - Allows "Time-travel debugging" using chrome extension
- Flexible - Works with any UI & has addons like thunk, saga, etc
- Tiny - Just 2kb (without RTK Query)
When to use ?
- Context/hooks - Small to mid apps, forms
- Redux - Very large and Complex apps
TradeOffs
- Lot of Boilerplate & setup
- Data fetching and caching is cumbersome.
- Manual
extraReducers
boilerplate. - Manual Query Data Memoization is complex using Reselect. (Memoization means updating store only if data has changed.)
Data fetching and caching
- Avoid using Thunks if needed.
- Libraries -
Apollo Client, React Query, Urql, and SWR
- See comparison (opens new window)
# 3 Principles of Redux
- Single source of truth - Single global object as store
- State is read-only - Only action can change state
- Changes are made with pure functions - Reducers are pure functions
# Pure vs Impure function
|
|
# Typescript
- Strongly recommended
- Typescript + redux-toolkit
// app/store.ts
import {configureStore} 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>;
// app/hooks.ts
import {TypedUseSelectorHook, useSelector, useDispatch} from 'react-redux';
import {RootState, AppDispatch} from './store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// features/Counter/counterSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "./../../app/store";
interface State {
/*...*/
}
const initialState: State = {
/*...*/
};
const counterSlice = createSlice({
name: 'counter',
initialState,
/*...*/
reducers:{
// action.payload is of type number
incrementBy: (state, action:PayloadAction<number>) => {
/*...*/
}
})
// actions
export const { increment, decrement..... } = counterSlice.actions;
// selectors
export const selectCount = (state:RootState) => state.counter.value;
# Redux Toolkit
- It's an abstraction layer that wraps around redux core
- Includes -
redux, immer, redux-thunk, reselect
react-redux
is not included
# Existing project
npm install @reduxjs/toolkit
# New project
# template = @reduxjs/toolkit + react-redux
yarn create react-app my-app --template redux
yarn create react-app my-app --template redux-typescript
# API
configureStore()
- A wrapper for redux
createStore
- Automatically adds slice, middlewares, devtools, etc
- A wrapper for redux
createReducer()
- A function used instead of switch case
- Use
immer
behind the scenes
createAction()
- A function based on action string
createSlice()
- Accepts - reducer functions, name, intialState
- Generates - slice reducer, action creators
createAsyncThunk
- Accepts - action type, function(which returns promise)
- Generates - a thunk with 3 action types
pending/fulfilled/rejected
createEntityAdapter
- Manage normalization (opens new window)
- "Normalization" means no duplication of data, and keeping items stored in a lookup table by item ID
- Normalized state shape usually looks like
{ids: [], entities: {}}
- Generates - reusable reducers and selectors
- Manage normalization (opens new window)
createSelector
- A utility from
reselect
- Generates memoized selectors that will only recalculate results when the inputs change ie caching.
- A utility from
Redux needs immutable state
Toolkit use immer
internally which allows us to write mutable code.
createSlice
&createReducer
use immer internally
# React-redux
- React binding for redux.
- It uses hooks api. It is recommended instead of
connect()
api
import {Provider} from 'react-redux';
import store from './store';
// wrap App in Provider
<Provider store={store}>
<App />
</Provider>;
// Get data from store in component
import {useSelector, useDispatch} from 'react-redux';
import {increment, selectCount} from './counterSlice';
// use it like normal hooks
const count = useSelector(selectCount);
const dispatch = useDispatch(); // dipatch(increment())
# Api
<Provider store={store}>
- Hooks
useSelector(), useDispatch(), useStore()
useActions()
- removed in v7.1
# Redux
- Actions are like events
- Reducers are like event listeners
- Dispatch is like trigger to events
- Subscribers are like listeners to changes after reducers
# Store
- Just an object
- Typically a redux app have just 1 store used globally.
getState
- get current state valuesubscribe
- update UI if state changes (returns a function to unsubscribe)dispatch
- dispatch an action
|
|
# Reducer
initialState
- can be{}, [], primitives
- Just a function.
- Signature -
(state, action) => newState
- Store ie
state
can be{}, [], primitives
- We can split root reducer into many separate reducers just like react components.
|
|
# Action
- Just an object
type
is compulsorypayload
is convention
const action = {
type: 'foo/A',
payload: {
id: 3,
name: 'umesh',
},
};
// action creator
const actionCreator = (id, name) => {
return {
type: 'foo/A',
payload: {
id: id,
name: name,
},
};
};
// action creator from slice
const {A, B} = fooSlice.actions;
# Async Thunks
Note : Use react-query like library. Also redux/toolkit comes with RTK Query
- Just a function with async logic.
- Redux Toolkit automatically sets up a middleware
redux-thunk
- Thunk function always gets args
(dispatch, getState)
createAsyncThunk() api
pending/fulfilled/rejected
- Automatically, action is created & dispatched too
{ type: "posts/fetchPost/pending", payload: returnValue }
- Usage - show/hide loading spinner, errors, etc in components
// REDUX TOOLKIT
// postSlice.js
const initialState = {
posts: [];
status: 'idle',
error: null
}
const fetchPosts = createAsyncThunk("posts/fetchPost", async (sendDataIfAny) => {
// also use try/catch
const response = await fetchPost("api/posts");
// return promise
return response;
// OR
// return data
const json = await response.json();
return json;
});
const postSlice = createSlice({
name: "post",
initialState,
reducers: {
/*...*/
},
extraReducers: {
[fetchPosts.pending]:(state, action) => {
state.status = "loading";
},
[fetchPosts.fulfilled]:(state, action) => {
state.status = "success";
state.posts = action.payload;
},
[fetchPosts.rejected]:(state, action) => {
state.status = "failed";
state.error = action.error.message;
}
},
// # Another way
extraReducers(builder){
builder
.addCase(fetchPost.pending, (state,action) => { /*..*/ })
.addCase(fetchPost.fulfilled, (state,action) => { /*..*/ })
.addCase(fetchPost.rejected, (state,action) => { /*..*/ })
}
})
# useSelector
- If redux state changes then
useSelector
will check if selected valuestate.counter.value
has changed. If changed only then re-render component.
// fooSlice.js
export const selectCount = (state) => state.counter.value;
// any react component
const count = useSelector(selectCount);
// OR without export
const count = useSelector((state) => state.counter.value);
# Ecosystem
https://redux.js.org/introduction/ecosystem (opens new window)
Only important and possibly useful list of ecosystem (other libraries are omitted) :
- Library Integration and Bindings -
react-redux
- Reducers -
redux-undo
- Actions -
redux-actions
- utility -
reselect, normalizr
- Store -
redux-persist
- Immutable -
immer
- sideeffects -
redux-thunk, redux-saga, redux-observable
- Middleware -
NONE
- Entities and Collections -
NONE
- Component State and Encapsulation -
NONE
- Devtools -
redux DevTools chrome extension
- Testing -
redux-mock-store
- Routing -
connected-react-router
- Forms -
redux-form
- Higher-Level Abstractions -
NONE