# ⚪️ React Redux を基礎から理解する
# Redux の基本概念
Redux Overview:
- Redux serves as an alternative to React Context, offering a centralized data store for the entire application.
- The store manages all states, eg. critical data like authentication status and user input states.
- It provides a unified state management approach by acting as a single store applicable to the entire application.
Subscription to Central Store:
- Components subscribe to the central store for updates.
- Subscribed components receive notifications when data changes occur in the Redux store.
- Components selectively retrieve needed data, such as authentication status, from specific portions of the Redux store.
- This establishes a unidirectional flow, enabling components to access and utilize data provided by the Redux store.
Important Rule:
- Components refrain from direct data manipulation.
- Subscriptions are established for components to receive updates, ensuring an indirect and controlled data flow.
- Components avoid engaging in a direct data flow towards the storage.
Introducing Reducers:
- Reducers handle data mutations and changes within the stored data.
- The term "reducer" in Redux differs from the reducer hook in React.
- A reducer function takes input, transforms it, and produces a new output, adhering to general programming concepts.
Understanding Reducer Functions:
- Redux reducer functions accept input, transform it, and generate a new output.
- They play a crucial role in updating stored data, following general programming principles.
Components and Actions:
- Components do not directly manipulate stored data; they use subscriptions for updates.
- Actions, triggered by components, describe the type of operation a reducer should execute.
- Components dispatch actions as simple JavaScript objects, specifying the operation type.
- Redux forwards actions to the appropriate reducer, which performs the specified operation.
- The reducer outputs a new state, effectively replacing the existing state in the central data store.
- Subscribed components receive notifications of state updates, facilitating UI refresh.
Three Principles of Redux:
- Single Data Source: The entire application's state is stored in a single store.
- State is Read-Only: State changes occur only through triggering actions, containing a required type attribute.
- Use Pure Functions for Actions: Reducers, implemented as pure functions, describe how actions change the state tree.
Store and Reducer Relationship:
- The store manages data, and its content is determined by the reducer function.
- The reducer function produces a new state snapshot whenever an action reaches it.
- Upon the initial code execution, the reducer also executes with a default action, typically setting the initial state.
- It is crucial for the reducer function to return a new state object consistently.
- The reducer function is a standard JavaScript function, always receiving the old/existing state and the dispatched action.
Reducer Function Structure:
- The reducer function is a JavaScript function, typically created using arrow function syntax.
- It must always return a new state object, ensuring it adheres to the principles of a pure function.
- Pure functions guarantee that the same inputs yield the same outputs and have no internal side effects.
Default State in Reducer:
- During the first execution, when the store initializes, the state might be undefined.
- Provide a default value for the state parameter in the reducer function to handle this initial case.
- The default value ensures that the state is set to an initial value, preventing undefined errors.
Creating and Subscribing to Store:
- Components subscribe to the store using a subscriber function.
- The subscriber function is notified whenever data and the store change.
- Use the
subscribe
method on the store, passing the subscriber function, to establish the subscription.
Dispatching Actions:
- Dispatch actions using the
dispatch
method on the store. - Actions are JavaScript objects with a
type
attribute acting as an identifier. - The
type
attribute should be a unique string, representing the type of action to be performed.
- Dispatch actions using the
Sample Action and State Update:
- Example of dispatching an action to increment a counter.
- The dispatched action contains a
type
attribute indicating an increment action. - The reducer function interprets the action type and produces a new state with an incremented counter.
- Subscribed components are notified of the state update.
Understanding Output:
- Executing the code demonstrates the incrementing counter as actions are dispatched.
- The store initialization and dispatching actions lead to state updates reflected in the output.
# Redux's Core API:
Redux.createStore(reducer, [preloadedState], [enhancer])
store.dispatch(action)
store.subscribe(listener)
store.getState()
store.replaceReducer(nextReducer)
# createStore
createStore(reducer, [preloadedState], [enhancer])
Creates a Redux store that holds the complete state tree of the app.
There should only be a single store in the app.
createStore accepts three parameters:
- reducer: A reducing function that returns the next state tree, given the current state tree and an action to handle.
- [preloadedState]: The initial state.
- [enhancer]: The store enhancer. You may optionally specify it to enhance the store with third-party capabilities such as middleware, time travel, persistence, etc. The only store enhancer that ships with Redux is
applyMiddleware()
.
store/index.js
import { createStore } from "redux";
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === "increment") {
return {
counter: state.counter + 1,
};
}
if (action.type === "decrement") {
return {
counter: state.counter - 1,
};
}
return state;
};
const store = createStore(counterReducer);
export default store;
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import "./index.css";
import App from "./App";
import store from "./store/index";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />
</Provider>
);
Counter.js
import classes from "./Counter.module.css";
import { useSelector, useDispatch } from "react-redux";
const Counter = () => {
// Again, this function will be executed for us by React Redux. it will then pass the Redux state in order to manage the data into this function when it is executed, and then basically execute this code to retrieve the state portion of this component that is needed. then use select or overall to return the value. useSelector((state) => state.counter);
const counter = useSelector((state) => state.counter);
// Redux automatically sets up a subscription to the Redux store for this component. So whenever the data in the Redux store changes, your component will be updated and automatically receive the latest counter. So it's an automatic reaction that changes to the Redux store will cause this component function to be re-executed. So you'll always have the most up-to-date counter.
const dispatch = useDispatch();
// dispatch is a function we can call, that will dispatch an action on our Redux store.
const decrementHandler = () => {
dispatch({ type: "decrement" });
};
const toggleCounterHandler = () => {};
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
<div className={classes.value}>{counter}</div>
<div>
<button onClick={incrementHandler}>Increment</button>
<button onClick={decrementHandler}>Decrement</button>
</div>
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;
# useSelector
In React Redux, the useSelector
hook requires two parameters:
Selector Function:
- This is a function that takes the entire Redux state as a parameter and returns the portion of the state you want to extract. For example, if you have a Redux state tree with an object named
stock
, and within thestock
object, there's a property namedcounter
, your selector function might be(state) => state.stock.counter
. This function determines the data thatuseSelector
will return.
- This is a function that takes the entire Redux state as a parameter and returns the portion of the state you want to extract. For example, if you have a Redux state tree with an object named
Equality Function (Optional):
- This is an optional parameter used to compare the values returned by the selector function in consecutive calls to determine if the component should be re-rendered. If omitted,
useSelector
will use reference equality (===
), meaning it will re-render only if the object references from the previous and current calls are the same. If you need to customize when the component should re-render based on some condition, you can provide a custom equality function.
- This is an optional parameter used to compare the values returned by the selector function in consecutive calls to determine if the component should be re-rendered. If omitted,
For example:
const counter = useSelector(
(state) => state.stock.counter,
(prev, next) => prev === next
);
In this example, the first parameter is the selector function, extracting state.stock.counter
. The second parameter is a custom equality function that uses reference equality to determine whether to re-render the component based on the returned values from the selector function.
# dispatch
const dispatch = useDispatch();
// dispatch is a function we can call, that will dispatch an action on our Redux store.
const decrementHandler = () => {
dispatch({ type: "decrement" });
//Attaching Payloads to Actions
};
# Redux Toolkit (opens new window)
When our application grows in complexity, using Redux can become more intricate. In this course, we'll explore a simpler approach to utilizing Redux. Before we proceed, let's consider some potential issues:
Potential Issues:
Action Type Identifiers:
- As the application grows, there may be numerous operations, leading to confusion with identifiers.
- Issues such as misspelling or conflicts in identifiers might arise.
Data Volume Management:
- With an increase in data volume, the state object becomes larger.
- The Reducer function becomes more complex and may be challenging to maintain.
Respecting State Immutability:
- Ensuring the consistent return of a new state snapshot and avoiding unintentional alterations to the existing state.
- Complex nested object and array data can lead to unpredictable state changes.
Solutions:
Unique Identifier Issue:
- Use constants to store identifiers, avoiding spelling errors and ensuring type consistency.
Data Volume Management and Complex Reducer Issue:
- Redux Toolkit provides solutions, such as splitting the Reducer into smaller ones.
State Immutability Issue:
- Manual implementation of solutions is possible, or Redux Toolkit tools can be used to automate state copying, ensuring unintentional state edits are avoided.
# Redux Toolkit's Core APIs:
configureStore()
: wrapscreateStore
to provide simplified configuration options and good defaults. It can automatically combine your slice reducers, adds whatever Redux middleware you supply, includesredux-thunk
by default, and enables use of the Redux DevTools Extension.createReducer()
: that lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses theimmer
library to let you write simpler immutable updates with normal mutative code, likestate.todos[3].completed = true
.createAction()
: generates an action creator function for the given action type string. The function itself hastoString()
defined, so that it can be used in place of the type constant.createSlice()
: accepts an object of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.createAsyncThunk
: accepts an action type string and a function that returns a promise, and generates a thunk that dispatches pending/fulfilled/rejected action types based on that promise
# State management with Redux
Redux Store Setup:
- Purpose: This section is responsible for setting up the global Redux store using the
configureStore
function from the@reduxjs/toolkit
library. The store is configured with the combined reducers that handle different parts of the application state.
- Purpose: This section is responsible for setting up the global Redux store using the
Wrap the App with the Redux Provider:
- Purpose: The
Provider
component fromreact-redux
is wrapped around the main application component (in this case,App
). This ensures that the Redux store is accessible to all components within the application. It essentially "provides" the Redux store to the entire component tree.
- Purpose: The
Creating a Redux Slice:
- Purpose: A Redux slice is a unit of the Redux state that corresponds to a specific feature or part of the application. In this example, the
exampleSlice
manages a list of items with corresponding actions like adding and removing items. It provides a clean and organized way to define the initial state and actions related to a specific feature.
- Purpose: A Redux slice is a unit of the Redux state that corresponds to a specific feature or part of the application. In this example, the
Using Redux in a Component:
- Purpose: This section demonstrates how to use the
useSelector
hook to access the Redux store's state within a React component. In the example,SomeComponent
usesuseSelector
to retrieve a list of items from the Redux store and render them.
- Purpose: This section demonstrates how to use the
Dispatching Actions in Another Component:
- Purpose: This section shows how to use the
useDispatch
hook to dispatch actions to the Redux store from a React component. In the example,AnotherComponent
allows the user to input a new item, and when a button is clicked, theaddItem
action is dispatched to add the new item to the Redux store.
- Purpose: This section shows how to use the
# Process of using Redux Toolkit
This setup provides a basic structure for handling state management with Redux in a JavaScript app. Action creators generate actions, reducers specify how the state should change, and the store manages the overall application state. The useSelector
hook is used to access the state, and useDispatch
is used to dispatch actions.
🟦 Create the Redux Store:
// store.js
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./reducers"; // Import your combined reducers
const store = configureStore({
reducer: rootReducer,
});
export default store;
🟦 Wrap the App with the Redux Provider:
// index.js or App.js
import React from "react";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";
const MainApp = () => {
return (
<Provider store={store}>
<App />
</Provider>
);
};
export default MainApp;
🟦 Creating a Redux Slice:
// exampleSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
entities: {},
ids: [],
};
export const exampleSlice = createSlice({
name: "example",
initialState,
reducers: {
addItem: (state, action) => {
const { id, data } = action.payload;
state.entities[id] = data;
state.ids.push(id);
},
removeItem: (state, action) => {
const idToRemove = action.payload;
delete state.entities[idToRemove];
state.ids = state.ids.filter((id) => id !== idToRemove);
},
},
});
export const { addItem, removeItem } = exampleSlice.actions;
export default exampleSlice.reducer;
🟦 Using Redux in a Component:
// SomeComponent.js
import React from "react";
import { useSelector } from "react-redux";
function SomeComponent() {
const items = useSelector((state) =>
state.example.ids.map((id) => state.example.entities[id])
);
return (
<div>
{items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
export default SomeComponent;
🟦 Dispatching Actions in Another Component:
// AnotherComponent.js
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addItem } from "./exampleSlice";
function AnotherComponent() {
const dispatch = useDispatch();
const [newItem, setNewItem] = useState("");
const handleAddItem = () => {
const id = Math.random().toString(36).substring(7); // Generate a unique ID
const data = { id, name: newItem }; // Example data structure
dispatch(addItem({ id, data }));
setNewItem("");
};
return (
<div>
<input
type="text"
value={newItem}
onChange={(e) => setNewItem(e.target.value)}
/>
<button onClick={handleAddItem}>Add Item</button>
</div>
);
}
export default AnotherComponent;
🔵 useSelector(): Accessing State
- Purpose:
useSelector
is a React hook provided by thereact-redux
library. Its primary purpose is to select and access the current state from the Redux store. It takes a selector function as an argument, which allows you to extract specific pieces of data from the state. By usinguseSelector
, components can efficiently subscribe to changes in the Redux state and re-render when relevant data is updated.
🔵 useDispatch(): Modifying State with Actions
- Purpose:
useDispatch
is another React hook from thereact-redux
library. Its primary purpose is to provide a reference to thedispatch
function of the Redux store. This allows components to dispatch actions, which are plain JavaScript objects containing atype
property and, optionally, apayload
. By usinguseDispatch
, components can trigger state changes by dispatching actions. These actions are then processed by reducers, modifying the state in a predictable and controlled manner.
🔵 Summary:
- Redux Store Setup: Configured a global store using
configureStore
from@reduxjs/toolkit
. - Provider Usage: Wrapped the main application component with
Provider
to make the Redux store accessible throughout the component tree. - Redux Slice Creation: Created a Redux slice using
createSlice
to manage a list of items in the state. - Component Interaction: Used
useSelector
to access state data anduseDispatch
to dispatch actions from React components.
# Handling Asynchronous Code:
Redux Core Principle:
- Reducer functions in Redux must be pure, without side effects, and synchronous.
- Pure functions produce the same output for the same input, ensuring consistency and predictability.
Handling Asynchronous Code:
- A challenge arises when dealing with asynchronous actions, like HTTP requests, in Redux.
- Reducer functions are unsuitable for asynchronous code due to their synchronous nature.
Options for Handling Asynchronous Code:
Component-Level Side Effects:
- Place side effect code, including asynchronous operations, directly in the component.
- Dispatch actions after the side effect completion to inform Redux.
Custom Action Creators:
- Write custom action creators for handling asynchronous tasks without altering the reducer function.
- Allows running asynchronous tasks as part of the action creator.
Redux Toolkit Solution:
- Redux Toolkit provides a solution for handling asynchronous tasks within action creators.
- It allows the execution of side effects without violating the synchronous nature of reducer functions.
# Redux Thunk
Redux Thunk:
- Redux Thunk is a middleware that allows the execution of asynchronous tasks in Redux.
- It enables the dispatch of asynchronous actions, such as HTTP requests, in Redux.
export const sendCartData = (cart) => {
//instead of return an action object, 如 { type: 'SOME_ACTION', payload: someData }。 我們創建一個一個action creator return another function
return async (dispatch) => {
dispatch(
uiActions.showNotification({
status: "pending",
title: "Sending...",
message: "Sending cart data!",
})
);
const sendRequest = async () => {
const response = await fetch(
"https://react-http-6b4a6.firebaseio.com/cart.json",
{
method: "PUT",
body: JSON.stringify(cart),
}
);
if (!response.ok) {
throw new Error("Sending cart data failed.");
}
};
try {
await sendRequest();
dispatch(
uiActions.showNotification({
status: "success",
title: "Success!",
message: "Sent cart data successfully!",
})
);
} catch (error) {
dispatch(
uiActions.showNotification({
status: "error",
title: "Error!",
message: "Sending cart data failed!",
})
);
}
};
};
The provided code demonstrates the use of a Redux thunk as an action creator. In contrast to regular action creators, which directly return an action object (e.g., { type: 'SOME_ACTION', payload: someData }
), this action creator returns a function.
Here are some key differences in this approach:
Returning a Function Instead of a Direct Action Object:
- Regular action creators typically return a plain action object. In this case, the action creator returns a function.
Incorporating Asynchronous Logic:
- The returned function contains asynchronous logic. In this example, it involves sending shopping cart data to a server using
async/await
with thefetch
function to make a PUT request.
- The returned function contains asynchronous logic. In this example, it involves sending shopping cart data to a server using
Dispatching Actions Before and After Asynchronous Logic:
- Before the asynchronous logic starts, an action is dispatched to indicate that the data is pending ("pending" notification). After the asynchronous logic succeeds or fails, corresponding actions are dispatched to display notifications with success or error messages.
Error Handling:
- Errors are handled using a
try-catch
block. If an error occurs during the asynchronous logic, an action is dispatched to show an "error" notification along with the appropriate error message.
- Errors are handled using a
More Flexible Control Flow:
- Thunks provide a more flexible control flow, allowing you to dispatch different actions at different stages of asynchronous logic. For instance, dispatching a "pending" action at the start, a "success" action upon success, and an "error" action upon failure.
In summary, this approach enables action creators to execute more complex logic, including asynchronous operations, conditional checks, and error handling. Thunks allow you to abstract away this logic from the components, allowing them to focus on user interface interactions without having to deal directly with complex asynchronous or side-effect logic.