REDUX, REDUX TOOLKIT, STATE MANAGEMENT - rs-hash/Senior GitHub Wiki
REDUX, REDUX TOOLKIT, STATE MANAGEMENT, Immutable.js, Thunk, saga
- REDUX
-
REDUX TOOLKIT
-
REDUX TOOLKIT CONCEPTS
- We can create a Redux store using the Redux Toolkit configureStore API
- Redux logic is typically organized into files called "slices"
- Redux reducers must follow specific rules
- Async logic is typically written in special functions called "thunks"
- React-Redux allows React components to interact with a Redux store
- REDUX TOOLKIT EXAMPLE
- Use of Immer.js in Redux Toolkit
- createAsyncThunk
- What is RTK Query?
-
REDUX TOOLKIT CONCEPTS
- REDUX VS REDUX TOOLKIT
- a single centralized place to contain the global state in your application, and specific patterns to follow when updating that state to make the code predictable.
- Redux is a JS library for predictable and maintainable global state management.
- Redux is a pattern and library for managing and updating application state, using events called "actions". It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.
- 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
- 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
- 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
Step-by-Step Breakdown
- User clicks a button: This triggers an action dispatch.
- Action is dispatched to the Redux store: The store receives the action.
- Store runs the reducer function: The reducer processes the action and updates the state.
- Store notifies subscribed UI components: The UI updates based on the new state.
/*store.js*/
import { createStore } from 'redux';
const initialState = {
count: 0,
};
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'counter/increment':
return { count: state.count + 1 };
default:
return state;
}
}
const store = createStore(counterReducer);
export default store;
/*Counter.jsx*/
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
const Counter = () => {
const count = useSelector((state) => state.count);
const dispatch = useDispatch();
const handleIncrement = () => {
dispatch({ type: 'counter/increment' });
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
</div>
);
};
export default Counter;
/*App.jsx*/
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import Counter from './Counter';
const App = () => {
return (
<Provider store={store}>
<Counter />
</Provider>
);
};
export default App;
The Redux Toolkit package is intended to be the standard way to write Redux logic.
configureStore
accepts areducer function
as a named argument- configureStore automatically sets up the store with good default settings
- A "slice" contains the reducer logic and actions related to a specific feature / section of the Redux state
- Redux Toolkit's
createSlice
API generates action creators and action types for each individual reducer function you provide
- Should only calculate a new state value based on the state and action arguments
- Must make immutable updates by copying the existing state
- Cannot contain any asynchronous logic or other "side effects"
- Redux Toolkit's createSlice API uses
Immer to allow "mutating" immutable updates
- Thunks receive
dispatch
andgetState
as arguments- Redux Toolkit enables the
redux-thunk
middleware by default
- Wrapping the app with
<Provider store={store}>
enables all components to use the store- Global state should go in the Redux store, local state should stay in React components
/src
- index.js: the starting point for the app
- App.js: the top-level React component
/app
- store.js: creates the Redux store instance
/features
/counter
- Counter.js: a React component that shows the UI for the counter feature
- counterSlice.js: the Redux logic for the counter feature
npm install @reduxjs/toolkit
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export default configureStore({
reducer: {
counter: counterReducer,
},
});
-
The Redux store is created using the
configureStore
function from Redux Toolkit. configureStore requires that we pass in areducer argument
. -
Our application might be made up of many different features, and each of those features might have its own reducer function. When we call configureStore,
we can pass in all of the different reducers in an object
. The key names in the object will define the keys in our final state value. -
We have a file named features/counter/counterSlice.js that exports a reducer function for the counter logic. We can import that counterReducer function here, and include it when we create the store.
-
When we pass in an object like
{counter: counterReducer}
, that says that we want to have astate.counter section of our Redux state object
, and that we want the counterReducer function to be in charge of deciding if and how to update the state.counter section whenever an action is dispatched.
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
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
},
incrementByAmount: (state, action) => {
state.value += action.payload
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
- createSlice automatically generates action creators with the same names as the reducer functions we wrote. We can check that by calling one of them and seeing what it return
- createSlice uses a library called Immer inside. Immer uses a special JS tool called a Proxy to wrap the data you provide, and lets you write code that "mutates" that wrapped data. But, Immer tracks all the changes you've tried to make, and then uses that list of changes to return a safely immutably updated value, as if you'd written all the immutable update logic by hand.
console.log(counterSlice.actions.increment())
// {type: "counter/increment"}
A thunk is a specific kind of Redux function that can contain asynchronous logic. Thunks are written using two functions:
- An inside thunk function, which gets dispatch and getState as arguments
- The outside creator function, which creates and returns the thunk function
- However, using thunks requires that the redux-thunk middleware (a type of plugin for Redux) be added to the Redux store when it's created.
- Fortunately, Redux Toolkit's
configureStore function already sets that up for us automatically
, so we can go ahead and use thunks here.
// 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
export const incrementAsync = amount => dispatch => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
//We can use them the same way we use a typical Redux action creator:
store.dispatch(incrementAsync(5))
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
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
},
incrementByAmount: (state, action) => {
state.value += action.payload
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
// 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
export const incrementAsync = (amount) => (dispatch) => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
// 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) => state.counter.value)`
export const selectCount = (state) => state.counter.value
export default counterSlice.reducer
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
selectCount,
} from './counterSlice';
import styles from './Counter.module.css';
export function Counter() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const [incrementAmount, setIncrementAmount] = useState('2');
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</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(Number(incrementAmount) || 0))
}
>
Add Amount
</button>
<button
className={styles.asyncButton}
onClick={() => dispatch(incrementAsync(Number(incrementAmount) || 0))}
>
Add Async
</button>
</div>
</div>
);
}
- Redux Toolkit (RTK) integrates Immer.js internally. When you create slices and reducers using RTK's createSlice or createReducer, it uses Immer.js to allow writing reducers that "mutate" the state, but under the hood, these mutations result in immutably updated state.
Example with Redux Toolkit:
import { createSlice, configureStore } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1; // Looks like a mutation, but Immer makes it immutable
},
decrement: (state) => {
state.value -= 1;
},
},
});
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
store.dispatch(counterSlice.actions.increment());
console.log(store.getState().counter.value); // 1
- createAsyncThunk is a utility provided by Redux Toolkit to handle asynchronous operations. It helps to create thunks that automatically dispatch actions based on the lifecycle of a promise (pending, fulfilled, and rejected).
Steps to Use createAsyncThunk for Fetching API Data in Redux Toolkit
- Create an Async Thunk
- Create a Slice
- Configure the Store
- Use the Thunk in a Component
// features/posts/postsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
// Define the async thunk
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
return response.data;
});
const postsSlice = createSlice({
name: 'posts',
initialState: {
items: [],
status: 'idle',
error: null
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
},
});
export default postsSlice.reducer;
Use thunk in component
// App.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchPosts } from './features/posts/postsSlice';
const App = () => {
const dispatch = useDispatch();
const posts = useSelector((state) => state.posts.items);
const postStatus = useSelector((state) => state.posts.status);
const error = useSelector((state) => state.posts.error);
useEffect(() => {
if (postStatus === 'idle') {
dispatch(fetchPosts());
}
}, [postStatus, dispatch]);
let content;
if (postStatus === 'loading') {
content = <p>Loading...</p>;
} else if (postStatus === 'succeeded') {
content = (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
} else if (postStatus === 'failed') {
content = <p>{error}</p>;
}
return (
<div>
<h1>Posts</h1>
{content}
</div>
);
};
export default App;
- RTK Query is a powerful data-fetching and caching tool built into Redux Toolkit (RTK). It simplifies data fetching and synchronization with the Redux store. RTK Query is designed to handle common requirements like caching, background updates, and re-fetching data.
RTK Query is used to streamline the process of data fetching, caching, synchronization, and state management in Redux applications. It provides:
-
Automatic Caching
: Automatically caches the fetched data, reducing the need to refetch data. -
Background Fetching
: Fetches data in the background and keeps the data synchronized with the server. -
Auto-refetching
: Automatically refetches data when certain conditions change, such as query parameters. -
Optimistic Updates
: Allows for optimistic updates, where the UI is updated before the server response is received. -
Simplified API
: Provides a simplified API for data fetching compared to traditional Redux patterns.
// src/services/api.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com/' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => 'posts',
}),
}),
});
export const { useGetPostsQuery } = api;
- Boilerplate Code: Requires more boilerplate code for actions, reducers, and store configuration.
- Middleware Configuration: Requires manual setup of middleware like redux-thunk.
- Immutability: Requires developers to manually ensure state immutability.
- Data Fetching: Requires additional libraries or custom code for data fetching and caching.
- Reduced Boilerplate:
Uses createSlice, createAsyncThunk, and configureStore
to reduce boilerplate code. - Built-in Middleware: Includes
middleware like redux-thunk by default
, with easier configuration. - Immutability with Immer:
Uses Immer to allow "mutating" syntax
for immutable state updates. -
RTK Query
: Provides built-in support for data fetching and caching, reducing the need for additional libraries.