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
2
3
4
mkdir plain-redux
cd plain-redux
npm init
npm install redux
Data Flow
example #1 - simple flow
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
54
55
56
// ./app.js

// node.js not simple support import
// ./app.js
const { createStore } = require("redux");

const initialState = {
value: 0,
};

// #2 initiation store state, accept dispatch by type
function counterReducer(state = initialState, action) {
console.log("reciver action", action);
switch (action.type) {
case "plus":
return {
value: state.value + 1,
};
break;
case "milus":
return {
value: state.value - 1,
};
break;
default:
break;
}
return state;
}

// #1 產生 store
let store = createStore(counterReducer);


// #3 get state
// console.log(store);
console.log(" first state:", store.getState());

// #4 trigger by dispatch
store.dispatch({
type: "plus",
});

store.dispatch({
type: "plus",
});

// #5 get state
console.log(" second state:", store.getState());

store.dispatch({
type: "milus",
});

// srtore get state
console.log(" third state:", store.getState());

run

1
2
3
4
5
6
7
8
plain-redux>node app.js
reciver action { type: '@@redux/INITg.f.3.1.7' }
first state: { value: 0 }
reciver action { type: 'plus' }
reciver action { type: 'plus' }
second state: { value: 2 }
reciver action { type: 'milus' }
third state: { value: 1 }
example #2 - todos create and delete
  • store subscribe
  • process state additional data
  • action constants
  • action creator
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// ./app.js

// node.js not simple support import
const { createStore } = require("redux");

// action constants
const ActionTypes = {
ADD_TDDO: "add_toto",
DELETE_TODO: "delete_todo",
};

const initialState = {
email: "12345",
todos: [],
};

// add id
let todoId = 0;

function counterReducer(state = initialState, action) {
console.log("reciver action", action);
switch (action.type) {
case ActionTypes.ADD_TDDO:
return {
// process state additional data
...state,
todos: [
...state.todos,
{
// add id
id: todoId++,
name: action.payload.name,
},
],
};
break;
// delete todo
case ActionTypes.DELETE_TODO:
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload.id),
};
break;
default:
break;
}
return state;
}

let store = createStore(counterReducer);

// add store subscribe
store.subscribe(() => {
console.log(" changed : ", store.getState());
});

// action creator
function addTodo(name) {
return {
type: ActionTypes.ADD_TDDO,
payload: {
name: name,
},
};
}

function deleteTodo(id) {
return {
type: ActionTypes.DELETE_TODO,
payload: {
id: id,
},
};
}

// add tdod
store.dispatch(addTodo("todo0"));
store.dispatch(addTodo("todo1"));
store.dispatch(addTodo("todo2"));
store.dispatch(addTodo("todo3"));

// delete todo
store.dispatch(deleteTodo(2));

run

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
>node app.js
reciver action { type: '@@redux/INIT1.n.3.f.v.b' }
reciver action { type: 'add_toto', payload: { name: 'todo0' } }
changed : { email: '12345', todos: [ { id: 0, name: 'todo0' } ] }
reciver action { type: 'add_toto', payload: { name: 'todo1' } }
changed : {
email: '12345',
todos: [ { id: 0, name: 'todo0' }, { id: 1, name: 'todo1' } ]
}
reciver action { type: 'add_toto', payload: { name: 'todo2' } }
changed : {
email: '12345',
todos: [
{ id: 0, name: 'todo0' },
{ id: 1, name: 'todo1' },
{ id: 2, name: 'todo2' }
]
}
reciver action { type: 'add_toto', payload: { name: 'todo3' } }
changed : {
email: '12345',
todos: [
{ id: 0, name: 'todo0' },
{ id: 1, name: 'todo1' },
{ id: 2, name: 'todo2' },
{ id: 3, name: 'todo3' }
]
}
reciver action { type: 'delete_todo', payload: { id: 2 } }
changed : {
email: '12345',
todos: [
{ id: 0, name: 'todo0' },
{ id: 1, name: 'todo1' },
{ id: 3, name: 'todo3' }
]
}

react-redux

install
1
2
3
4
npx create-react-app react-dedux-demo --template redux
cd react-dedux-demo
# 已安裝
# npm install react-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ./src/App.js
import { selectTodos } from "./redux/selectors";
import { useSelector } from "react-redux";
import AddTodo from "./AddTodo";

function App() {
const todos = useSelector(selectTodos);
console.log("todos", todos);
return (
<div>
<AddTodo />
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.id} {todo.name}
</li>
))}
</ul>
</div>
);
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ./src/AddTodo.js
import { useDispatch } from "react-redux";
import { addTodo } from "./redux/actions";

export default function AddTodo() {
const dispatch = useDispatch();
return (
<button
onClick={() => {
dispatch(addTodo(Math.random()));
}}
>
add todo
</button>
);
}
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
2
3
4
5
6
# 已自動安裝 Redux Toolkit
npx create-react-app react-dedux-demo --template redux
# 若不安裝 template
npx create-react-app my-app
cd my-app
npm install @reduxjs/toolkit

進階

redux middleware

Data Flow
middleware example and add redux devtool
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/store.js

// step #61 add middleware - import
import { createStore, applyMiddleware } from "redux";
// step #71 add for redux devtool with middle - import
import { compose } from "redux";
import rootReducer from "./reducers";

// step #72 add for redux devtool with middle - variable
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

// step #62 add middleware - middleware 1
const logMiddleWare1 = (store) => (next) => (action) => {
console.log("Log Middleware1:", action);
next(action);
};

// step #63 add middleware - middleware 2
const logMiddleWare2 = (store) => (next) => (action) => {
console.log("Log Middleware2:", action);
next(action);
};

// step #3 產生 store(from ./reducers )
export const store = createStore(
rootReducer,
// step #73 add for redux devtool with middle - add
// step #64 add middleware - add
composeEnhancers(applyMiddleware(logMiddleWare1, logMiddleWare2))
// step #41 add for redux devtool
// window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
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
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
54
55
56
57
58
59
60
61
62
// ./src/App.js
import { selectTodos } from "./redux/selectors";
import { useDispatch, useSelector } from "react-redux";
// import AddTodo from "./containers/AddTodo";
import { addTodo } from "./redux/actions";
// step #65 thunk - import async action creator
import { addTodoAsync, AddTodoFetchPosts } from "./redux/actions";
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 /> */}
<button
// step #11 dispatch 呼叫 action creator, 產生 action
onClick={() => {
dispatch(addTodo(Math.random()));
}}
>
add todo
</button>
<button
onClick={() => {
// step #66 thunk - dispatch 呼叫 async action creator
dispatch(addTodoAsync(Math.random()));
}}
>
add todo async
</button>
<button
onClick={() => {
// step #67 thunk - dispatch 呼叫 fetch action creator
dispatch(AddTodoFetchPosts());
}}
>
add toto fetch
</button>
<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;
./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
54
55
56
57
// ./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,
},
};
}

// step #63 thunk - async action
export function addTodoAsync(name) {
return (dispatch) => {
setTimeout(() => {
// 一秒後dispatch addTask()
dispatch(addTodo(name));
}, 1000);
};
}

// step #64 thunk - fetch action
export function AddTodoFetchPosts() {
return (dispatch) => {
return fetch(
"https://api.kcg.gov.tw/api/service/Get/b4e6ae98-39b7-469b-8c68-56492cad3b71"
)
.then((res) => res.json())
.then((json) => {
// console.log(json.data[0]);
// console.log(typeof json.data[0]);
dispatch(addTodo(json.data[0]["市場地址"]));
});
};
}
./src/redux/store.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ./src/redux/store.js

import { createStore } from "redux";
import rootReducer from "./reducers";
// step #61 thunk - import thunk
import { applyMiddleware } from "redux";
import thunk from "redux-thunk";
// step #71 redux-logger - import
import { createLogger } from "redux-logger";

// step #72 redux-logger
const loggerMiddleware = createLogger();

// step #3 產生 store(from ./reducers )
export const store = createStore(
rootReducer,
// step #62 thunk - set at middleware
// step #73 redux-logger - add to middle
applyMiddleware(thunk, loggerMiddleware)
// step #41 add for redux devtool
// window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ./src/index.js
// step #21 redux(get post) - do for blog(顯示單篇文章)
import React from "react";
import ReactDOM from "react-dom";
// import App from "./components/Signup";
// step #1 change to blog
import App from "./components/App";
// step #21 redux(get post) - import store, Provider
import { store } from "./redux/store";
import { Provider } from "react-redux";

ReactDOM.render(
// step #22 redux(get post) - Provider store
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
./src/Pages/BlogPost/BlogPost.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
// ./Pages/BlogPost/BlogPost.js
import React, { useEffect } from "react";
import styled from "styled-components";
import { useParams } from "react-router-dom";
// step #29 redux(get post) - import useSelector, useSelector, actions, selectors
import { useDispatch, useSelector } from "react-redux";
import { addPostFromFetch, deletePostFromFetch } from "../../redux/actions";
import { selectPost } from "../../redux/selectors";

const PostContainer = styled.div`
width: 80%;
margin: 0 auto;
`;
const PostTitle = styled.div`
text-align: center;
font-size: 24px;
`;
const PostBody = styled.div``;

export default function BlogPost() {
let { id } = useParams();
// const state = useSelector((state) => state);
// console.log("state:", state);

const post = useSelector(selectPost);
const dispatch = useDispatch();
// console.log(post);

useEffect(() => {
// step #30 redux - trigger dispatch addPostFromFetch action
dispatch(addPostFromFetch(id));
}, [dispatch, id]);

// step #42 add delete blog item - handel function
const handleDelete = async (e) => {
await dispatch(deletePostFromFetch(id));
await console.log(id);
// step #45 add delete blog item - jump to home page
await window.location.assign("/");
};

return (
<PostContainer>
{/* step #41 add delete blog item - button */}
<button onClick={handleDelete}>刪除</button>
<PostTitle>{post.title}</PostTitle>
<PostBody>{post.body}</PostBody>
</PostContainer>
);
}
./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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ./src/index.js
// step #20 redux(get post) - do for blog(顯示單篇文章)
// step #50 redux(add blog) - 發表文章
import React from "react";
import ReactDOM from "react-dom";
// import App from "./components/Signup";
// step #1 change to blog
import App from "./components/App";
// step #21 redux(get post) - import store, Provider
import { store } from "./redux/store";
import { Provider } from "react-redux";

ReactDOM.render(
// step #22 redux(get post) - Provider store
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
./Pages/CreatePage/CreatePage.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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// ./Pages/CreatePage/CreatePage.js
import React, { useState } from "react";
import styled from "styled-components";
import { useHistory } from "react-router-dom";

import { useDispatch, useSelector } from "react-redux";
import { addPostFromFetch, clearPost } from "../../redux/actions";
import { selectPost, selectIsLoad } from "../../redux/selectors";
import { useEffect } from "react";

const Root = styled.div`
width: 80%;
margin: 0 auto;
`;

const Title = styled.div`
padding: 5px 0;
input {
width: 100%;
}

div {
padding-bottom: 5px;
}
`;

const Content = styled.div`
padding-bottom: 10px;

textarea {
width: 100%;
}

div {
padding-bottom: 5px;
}
`;

const ErrorMessage = styled.div`
color: red;
`;

export default function CreatePage() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [errorMessage] = useState();
const history = useHistory();
const dispatch = useDispatch();

const handleSubmit = (e) => {
e.preventDefault();
// step #59 redux(add blog) - submit add post
dispatch(addPostFromFetch(title, content));
// console.log("handleSubmit");
};

useEffect(() => {
// step #58 redux(add blog) - clear state when add post mount
dispatch(clearPost());
// console.log("creat mount..");
}, [dispatch]);

const post = useSelector(selectPost);
const isLoad = useSelector(selectIsLoad);
useEffect(() => {
// console.log("create effect :", post);
// console.log("isLoad:", isLoad);
// console.log("creat update ");
// step #60 redux(add blog) - show post after add post
if (isLoad) {
history.push("/posts/" + post.id);
}
}, [post, isLoad, history]);

return (
<Root>
<form onSubmit={handleSubmit}>
<Title>
<div>Title</div>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</Title>
<Content>
<div>Content</div>
<textarea
rows="10"
value={content}
onChange={(e) => setContent(e.target.value)}
></textarea>
</Content>
<div>
<button type="submit">新增</button>
</div>
{errorMessage && <ErrorMessage>{errorMessage}</ErrorMessage>}
</form>
</Root>
);
}
./Pages/BlogPost/BlogPost.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
54
55
56
57
58
59
60
61
62
63
64
// ./Pages/BlogPost/BlogPost.js
import React, { useEffect } from "react";
import styled from "styled-components";
import { useParams } from "react-router-dom";
// step #29 redux(get post) - import useSelector, useSelector, actions, selectors
import { useDispatch, useSelector } from "react-redux";
import {
getPostFromFetch,
deletePostFromFetch,
clearPost,
} from "../../redux/actions";
import { selectPost, selectIsLoad } from "../../redux/selectors";

const PostContainer = styled.div`
width: 80%;
margin: 0 auto;
`;
const PostTitle = styled.div`
text-align: center;
font-size: 24px;
`;
const PostBody = styled.div``;

export default function BlogPost() {
let { id } = useParams();
// const state = useSelector((state) => state);
// console.log("state:", state);

const post = useSelector(selectPost);
const isLoad = useSelector(selectIsLoad);
const dispatch = useDispatch();
// console.log(post);
// console.log("isLoad:", isLoad);

useEffect(() => {
return () => {
// step #57 redux(add blog) - clear state when get post unmount
dispatch(clearPost());
};
}, [dispatch]);

useEffect(() => {
// step #30 redux(get post) - trigger dispatch getPostFromFetch action
dispatch(getPostFromFetch(id));
}, [dispatch, id]);

// step #42 add delete blog item - handel function
const handleDelete = async (e) => {
await dispatch(deletePostFromFetch(id));
await console.log(id);
// step #45 add delete blog item - jump to home page
await window.location.assign("/");
};

return (
<PostContainer>
{/* step #41 add delete blog item - button */}
<button onClick={handleDelete}>刪除</button>
{/* step #31 redux(get post) - add no data options */}
{isLoad && <PostTitle>{post.title}</PostTitle>}
{isLoad && <PostBody>{post.body}</PostBody>}
</PostContainer>
);
}
./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__()
    );

參考資料

圖檔來源