Redux
名詞
常用 list
- createStore : 產生 store
- useSelector : Redux store 中的狀態提取數據
- useDispatch : 回傳一個 dispatch 方法, 產生 action
- Provider : 讓整個react applicaiton都能取得Redux store的資料
- combineReducers : 由多個不同 reducer 函數作為 value 的 object,合併成一個最終的 reducer 函數,然後就可以對這個 reducer 調用 createStore
- connect : 將傳入 components 的 states 和 actions 進行篩選或初步處理
基礎
Redux 基本操作
install
1 | mkdir plain-redux |
Data Flow
example #1 - simple flow
1 | // ./app.js |
run
1 | plain-redux>node app.js |
example #2 - todos create and delete
- store subscribe
- process state additional data
- action constants
- action creator
1 | // ./app.js |
run
1 | >node app.js |
react-redux
install
1 | npx create-react-app react-dedux-demo --template redux |
example #1 - todos
./src
./src/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// ./src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
// step #2 store patch
import { store } from "./redux/store";
import { Provider } from "react-redux";
ReactDOM.render(
// step #1 Provider 綁定,使整個react applicaiton都能取得 store
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);./src/App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39// ./src/App.js
import { selectTodos } from "./redux/selectors";
import { useSelector, useDispatch } from "react-redux";
import { addTodo } from "./redux/actions";
function App() {
const state = useSelector((state) => state);
console.log("state:", state);
// step #7 useSelector 取得 store 內的植
// todos = useSelector((store) => store.todoState.todos)
const todos = useSelector(selectTodos);
// step #9 useDispatch 可 trigger action
const dispatch = useDispatch();
// step #32 若已執行修改後會顯示,但直接執行(或重整畫面)則不會顯示 ???
console.log("todos", todos);
return (
<div>
<button
// step #11 dispatch 呼叫 action creator, 產生 action
onClick={() => {
dispatch(addTodo(Math.random()));
}}
>
add todo
</button>
<ul>
{/* step #12 show todos */}
{todos.map((todo) => (
<li key={todo.id}>
{todo.id} {todo.name}
</li>
))}
</ul>
</div>
);
}
export default App;
./src/redux
./src/redux/actionTypes.js
1
2
3
4
5// ./src/redux/actionTypes.js
export const ADD_TODO = "add_todo";
export const DELETE_TODO = "delete_todo";
export const ADD_USER = "add_user";./src/redux/actions.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32// ./src/redux/actions.js
import { ADD_TODO, DELETE_TODO, ADD_USER } from "./actionTypes";
// step #10 做成 action creator(function) 方便使用
// action creator
export function addTodo(name) {
return {
type: ADD_TODO,
payload: {
name: name,
},
};
}
export function deleteTodo(id) {
return {
type: DELETE_TODO,
payload: {
id: id,
},
};
}
export function addUser(name) {
return {
type: ADD_USER,
payload: {
name,
},
};
}./src/redux/store.js
1
2
3
4
5
6
7
8
9
10
11// ./src/redux/store.js
import { createStore } from "redux";
import rootReducer from "./reducers";
// step #3 產生 store(from ./reducers )
export const store = createStore(
rootReducer,
// step #31 add for redux devtool
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);./src/redux/selectors.js
1
2
3
4// ./src/redux/selectors.js
// step #8 selectot 另開成檔案
export const selectTodos = (store) => store.todoState.todos;
./src/redux/reducers
./src/redux/reducers/index.js
1
2
3
4
5
6
7
8
9
10// ./src/redux/reducers/index.js
import { combineReducers } from "redux";
import todos from "./todos";
import users from "./users";
// step #4 combineReducers 可包含 多個 reducer
export default combineReducers({
todoState: todos, //todos: todos
users, //users: users
});./src/redux/reducers/todos.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37// ./src/redux/reducers/todos.js
import { ADD_TODO, DELETE_TODO } from "../actionTypes";
let todoId = 0;
const initialState = {
todos: [],
};
// step #5 todosReducer
export default function todosReducer(state = initialState, action) {
console.log("reciver action(todos)", action);
switch (action.type) {
case ADD_TODO:
return {
// process state additional data
...state,
todos: [
...state.todos,
{
// add id
id: todoId++,
name: action.payload.name,
},
],
};
// delete todo
case DELETE_TODO:
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload.id),
};
default:
break;
}
return state;
}./src/redux/reducers/users.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// ./src/redux/reducers/users.js
import { ADD_USER } from "../actionTypes";
const initialState = {
users: [],
};
// step #6 usersReducer
export default function usersReducer(state = initialState, action) {
console.log("reciver action(users)", action);
switch (action.type) {
case ADD_USER:
return {
// process state additional data
...state,
users: [
...state.users,
{
name: action.payload.name,
},
],
};
default:
break;
}
return state;
}
example #2 - button create component
1 | // ./src/App.js |
1 | // ./src/AddTodo.js |
example #3 - use connect(無 hook 前使用的方法)
./src
./src/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// ./src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
// step #2 store patch
import { store } from "./redux/store";
import { Provider } from "react-redux";
ReactDOM.render(
// step #1 Provider 綁定,使整個react applicaiton都能取得 store
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);./src/App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// ./src/App.js
import { selectTodos } from "./redux/selectors";
import { useSelector } from "react-redux";
import AddTodo from "./containers/AddTodo";
function App() {
// step #7 useSelector 取得 store 內的植
// todos = useSelector((store) => store.todoState.todos)
const todos = useSelector(selectTodos);
// step #42 若已執行修改後會顯示,但直接執行(或重整畫面)則不會顯示 ???
console.log("todos", todos);
return (
<div>
{/* call step #21 addTodo component */}
<AddTodo />
<ul>
{/* step #9 show todos */}
{todos.map((todo) => (
<li key={todo.id}>
{todo.id} {todo.name}
</li>
))}
</ul>
</div>
);
}
export default App;
./src/redux
./src/redux/store.js
1
2
3
4
5
6
7
8
9
10
11// ./src/redux/store.js
import { createStore } from "redux";
import rootReducer from "./reducers";
// step #3 產生 store(from ./reducers )
export const store = createStore(
rootReducer,
// step #41 add for redux devtool
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);./src/redux/selectors.js
1
2
3
4// ./src/redux/selectors.js
// step #8 selectot 另開成檔案
export const selectTodos = (store) => store.todoState.todos;./src/redux/actionTypes.js
1
2
3
4
5// ./src/redux/actionTypes.js
export const ADD_TODO = "add_todo";
export const DELETE_TODO = "delete_todo";
export const ADD_USER = "add_user";./src/redux/actions.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32// ./src/redux/actions.js
import { ADD_TODO, DELETE_TODO, ADD_USER } from "./actionTypes";
// step #10 做成 action creator(function) 方便使用
// action creator
export function addTodo(name) {
return {
type: ADD_TODO,
payload: {
name: name,
},
};
}
export function deleteTodo(id) {
return {
type: DELETE_TODO,
payload: {
id: id,
},
};
}
export function addUser(name) {
return {
type: ADD_USER,
payload: {
name,
},
};
}
./src/redux/reducers
./src/redux/reducers/index.js
1
2
3
4
5
6
7
8
9
10// ./src/redux/reducers/index.js
import { combineReducers } from "redux";
import todos from "./todos";
import users from "./users";
// step #4 combineReducers 可包含 多個 reducer
export default combineReducers({
todoState: todos, //todos: todos
users, //users: users
});./src/redux/reducers/todos.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37// ./src/redux/reducers/todos.js
import { ADD_TODO, DELETE_TODO } from "../actionTypes";
let todoId = 0;
const initialState = {
todos: [],
};
// step #5 todosReducer
export default function todosReducer(state = initialState, action) {
console.log("reciver action(todos)", action);
switch (action.type) {
case ADD_TODO:
return {
// process state additional data
...state,
todos: [
...state.todos,
{
// add id
id: todoId++,
name: action.payload.name,
},
],
};
// delete todo
case DELETE_TODO:
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload.id),
};
default:
break;
}
return state;
}./src/redux/reducers/users.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// ./src/redux/reducers/users.js
import { ADD_USER } from "../actionTypes";
const initialState = {
users: [],
};
// step #6 usersReducer
export default function usersReducer(state = initialState, action) {
console.log("reciver action(users)", action);
switch (action.type) {
case ADD_USER:
return {
// process state additional data
...state,
users: [
...state.users,
{
name: action.payload.name,
},
],
};
default:
break;
}
return state;
}
./src/containers
- ./src/containers/AddTodo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33// ./src/containers/AddTodo.js
import { connect } from "react-redux";
import { addTodo } from "../redux/actions";
import AddTodo from "../components/AddTodo";
// step #23 connect parameter 1 ,使用 connect 來取得 Redux 中的 states(store 內的值)
const mapStateToProps = (store) => {
return {
todos: store.todoState.todos,
};
};
// step #24 connect parameter 2 , 使用 connect 來 dispatch actions
// const mapDispatchToProps = (dispatch) => {
// return {
// addTodo: (payload) => dispatch(addTodo(payload)),
// };
// };
// step #26 上面簡化(因 props 與 action 同名)
const mapDispatchToProps = {
addTodo,
};
// step #22 利用 connect 處理
// const connectToStore = connect(mapStateToProps, mapDispatchToProps);
// step #25 link addTodo component
// const ConnectedAddTodo = connectToStore(AddTodo);
// // HOC(Higher Order Component)
// // smart component or container(知道 redux)
// export default ConnectedAddTodo;
// step #27 等同上面三行 command
export default connect(mapStateToProps, mapDispatchToProps)(AddTodo);
./src/components
- ./src/components/AddTodo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// ./src/components/AddTodo.js
// step #28 addTodo component
// dump component(不知 redux)
export default function AddTodo({ addTodo }) {
return (
<button
// step #29 dispatch 呼叫 action creator, 產生 action
onClick={() => {
addTodo(Math.random());
}}
>
add todo
</button>
);
}
example #4 - todos example
- ./src/components/AddTodo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// ./src/components/AddTodo.js
import { useState, Fragment } from "react";
// step #28 addTodo component
// dump component(不知 redux)
export default function AddTodo({ addTodo }) {
// step #52 add state
const [value, setValue] = useState("");
return (
// step #51 add Fragment for multi element
// <Fragment></Fragment> 簡寫
<>
{/* step #53 add todo input */}
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button
// step #29 dispatch 呼叫 action creator, 產生 action
// step #54 input put to todos
onClick={() => {
addTodo(value);
setValue("");
}}
>
add todo
</button>
</>
);
} - ./src/App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35// ./src/App.js
import { selectTodos } from "./redux/selectors";
import { useDispatch, useSelector } from "react-redux";
import AddTodo from "./containers/AddTodo";
import { deleteTodo } from "./redux/actions";
function App() {
// step #7 useSelector 取得 store 內的植
// todos = useSelector((store) => store.todoState.todos)
const todos = useSelector(selectTodos);
// step #42 若已執行修改後會顯示,但直接執行(或重整畫面)則不會顯示 ???
console.log("todos", todos);
// step #56 add delete button - add dispatch
const dispatch = useDispatch();
return (
<div>
{/* call step #21 addTodo component */}
<AddTodo />
<ul>
{/* step #9 show todos */}
{todos.map((todo) => (
<li key={todo.id}>
{todo.id} {todo.name}
{/* step #55 add delete button */}
<button onClick={() => dispatch(deleteTodo(todo.id))}>
delete
</button>
</li>
))}
</ul>
</div>
);
}
export default App;
react-redux(Redux Toolkit )
boilerplate : 樣板
immer : 一个 immutable library,利用 ES6 的 proxy,以最小的成本實现了 js 的不可變數據結構
install
1 | # 已自動安裝 Redux Toolkit |
進階
redux middleware
Data Flow
middleware example and add redux devtool
1 | // ./src/redux/store.js |
thunk - example by async todos and fetch + redux-logger
Redux Thunk middleware 讓你可以撰寫一個回傳 function 而非 action 的 action creators,透過 thunk 可以讓你控制發送(dispatch)一個 action 的時間點,因此適合用來處理非同步取得的資料狀態,或者是在特定條件符合的情況下才發送。
install redux-logger
1 | npm install redux-logger |
./src/App.js
1 | // ./src/App.js |
./src/redux/actions.js
1 | // ./src/redux/actions.js |
./src/redux/store.js
1 | // ./src/redux/store.js |
Redux thunk example from template
./src/feature/counter/counterSlice.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17....
// 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. Thunks are
// typically used to make async requests.
export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount) => {
const response = await fetchCount(amount);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
...../src/feature/counter/counterAPI.js
1
2
3
4
5
6// A mock function to mimic making an async request for data
export function fetchCount(amount = 1) {
return new Promise((resolve) =>
setTimeout(() => resolve({ data: amount }), 500)
);
}
blog use redux example - 顯示單篇文章
./src/index.js
1 | // ./src/index.js |
./src/Pages/BlogPost/BlogPost.js
1 | // ./Pages/BlogPost/BlogPost.js |
./src/redux
./src/redux/actionTypes.js
1
2
3
4// ./src/redux/actionTypes.js
// step #25 redux(get post) - add actionTypes
export const GET_POST = "get_post";./src/redux/actions.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39// ./src/redux/actions.js
import { GET_POST } from "./actionTypes";
import { getPostId, deletePost } from "../WebApi";
// step #26 redux(get post) - add action addPost
export function addPost(title, body) {
return {
type: GET_POST,
payload: {
title,
body,
},
};
}
// step #27 redux(get post) - add action addPostFromFetch
export function addPostFromFetch(id) {
return (dispatch) => {
return getPostId(id).then((post) => {
// console.log(post);
// console.log(post[0]);
if (post.length > 0) {
dispatch(addPost(post[0].title, post[0].body));
}
});
};
}
// step #44 add delete blog item - action
export function deletePostFromFetch(id) {
return (dispatch) => {
return deletePost(id)
.then((res) => {
console.log("deletePostFromFetch", res);
})
.catch((e) => console.log("error=", e));
};
}
./src/redux/store.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// ./src/redux/store.js
import { createStore } from "redux";
import postReducer from "./reducers/postReducer";
// step #23 redux(get post) - import applyMiddleware, thunk, compose(redux devtool for middleware)
import { applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { compose } from "redux";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// step #24 redux(get post) - add store
export const store = createStore(
postReducer,
composeEnhancers(applyMiddleware(thunk))
);./src/redux/selectors.js
1
2
3
4// ./src/redux/selectors.js
// step #27 redux(get post) - add selector for get data
export const selectPost = (store) => store.post;
./src/redux/reducers
- ./src/redux/reducers/postReducer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// ./src/redux/reducers/postReducer.js
import { GET_POST } from "../actionTypes";
const initialState = {
post: {
title: "",
body: "",
},
};
// step #28 redux(get post) - add postReducer
export default function postReducer(state = initialState, action) {
console.log("receive action(post)", action);
switch (action.type) {
case GET_POST:
return {
post: {
title: action.payload.title,
body: action.payload.body,
},
};
default:
break;
}
return state;
}
blog use redux example - 發表文章
./src/index.js
1 | // ./src/index.js |
./Pages/CreatePage/CreatePage.js
1 | // ./Pages/CreatePage/CreatePage.js |
./Pages/BlogPost/BlogPost.js
1 | // ./Pages/BlogPost/BlogPost.js |
./src/redux
./src/redux/actionTypes.js
1
2
3
4
5
6
// step #25 redux(get post) - add actionTypes
export const GET_POST = "get_post";
// step #51 redux(add blog) - add actionTypes
export const ADD_POST = "add_post";
export const CLEAR_POST = "clear_post";./src/redux/actions.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53// ./src/redux/actions.js
import { CLEAR_POST, GET_POST } from "./actionTypes";
import { getPostId, deletePost, create } from "../WebApi";
// step #26 redux(get post) - add action getPost
export function getPost(post) {
return {
type: GET_POST,
payload: post,
};
}
export function clearPost() {
return {
type: CLEAR_POST,
payload: null,
};
}
// step #27 redux(get post) - add action getPostFromFetch
export function getPostFromFetch(id) {
return (dispatch) => {
return getPostId(id).then((post) => {
// console.log(post);
// console.log(post[0]);
if (post.length > 0) {
dispatch(getPost(post[0]));
}
});
};
}
// step #44 add delete blog item - action
export function deletePostFromFetch(id) {
return (dispatch) => {
return deletePost(id)
.then((res) => {
console.log("deletePostFromFetch", res);
})
.catch((e) => console.log("error=", e));
};
}
// step #52 redux(add blog) - add action
export function addPostFromFetch(title, body) {
return (dispatch) => {
return create(title, body).then((data) => {
// console.log("create:", data);
dispatch(getPost(data));
});
};
}./src/redux/store.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// ./src/redux/store.js
import { createStore } from "redux";
import postReducer from "./reducers/postReducer";
// step #23 redux(get post) - import applyMiddleware, thunk, compose(redux devtool for middleware)
import { applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { compose } from "redux";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// step #24 redux(get post) - add store
export const store = createStore(
postReducer,
composeEnhancers(applyMiddleware(thunk))
);
- ./src/redux/selectors.js
1
2
3
4
5
6// ./src/redux/selectors.js
// step #28 redux(get post) - add selector for get data
export const selectPost = (store) => store.post;
// step #56 redux(add blog) - add selector for isLoad flag
export const selectIsLoad = (store) => store.isLoad;
./src/redux/reducers
- ./src/redux/reducers/postReducer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// ./src/redux/reducers/postReducer.js
import { CLEAR_POST, GET_POST, ADD_POST } from "../actionTypes";
const initialState = {
// step #55 redux(add blog) - change state parameter
isLoad: false,
post: null,
};
// step #28 redux(get post) - add postReducer
export default function postReducer(state = initialState, action) {
// console.log("receive action(post)", action);
switch (action.type) {
// step #53 redux(add blog) - add reducer
case CLEAR_POST:
return {
isLoad: false,
post: null,
};
// step #54 redux(add blog) - add reducer
case GET_POST:
case ADD_POST:
return {
isLoad: true,
post: action.payload,
};
default:
break;
}
return state;
}
redux devtool
chrome install Redux DevTools
add code as bellow for restore
1
2
3
4
5
6
7
8
9// ./src/redux/store.js
import { createStore } from 'redux'
import rootReducer from './reducers'
export const store = createStore(
rootReducer,
// add for redux devtool
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
參考資料
- Redux
- Redux 中文
- Flux
- react-redux
- Configuring Your Store
- Presentational and Container Components
- A Todo List Example(Using the connect API)
- Redux Toolkit
- Redux 中文網
- Hooks 常見問題
- Redux-Saga
- redux-observable