Redux 不只能用在 react 身上,他是一個架構、概念。可以套用在任何東西上。
Redux 是實作狀態管理機制的套件,也可以在其他框架看到類似東西。
最早是從 facebook 提 REACT 的概念:FLUX 他是一個架構的東西。Redux 跟 FLUX 有點不一樣。很推薦閱讀 Redux 的官方文件 !
在認識 Redux 之前
狀態管理對前端非常重要 ! 可以想成資料與 UI 之間的對應關係。
對於程式狀態、資料、畫面,不同框架有不同想法
- Jquery:資料與畫面分開,思考點在怎麼把資料放到畫面上去。要拿資料時,因為東西全都放到畫面上了,要在從畫面上將資料拿回來。
- Vue and Angular:資料與畫面雙向綁定,可以把資料跟畫面的某部分綁在一起,其中一方改變對方都會變。
- React:只要管資料。react 會把 ui 刻出來,我們只要管資料與狀態。UI = f(state)。react 比較像是單向,改了 state 才會影響畫面,但改動畫面不會影響 state。
增加複雜度,解決問題:flux
flux 是一個架構相比較的概念為 MVC。最早出來,比 redux 更複雜。他是一個單向的資料流。
想解決的問題:傳統 MVC 中 model 與 view 可能會有很多複雜的關係,view 可以同時有很多 model 的資料,按按鈕會同時改動到很多不同 model 的資料。
當專案東西太大,就會有狀態的問題。比如 model 東西被改變,其實很難追蹤誰改變他。flux, redux 要解決的是規模很大的時候,狀態怎麼管理的問題。
ACTION (動作) => DISPATCHER (分發器) => STORE (資料) => VIEW (畫面)
今天要改變 STORE 裡面的資料,只能透過 DISPATCHER(分發器)。EX:按按鈕,VIEW 送 ACTION 到 DISPATCHER,DISPATCHER 再改變 STORE,STORE 再將更新的值傳回 VIEW。
對小軟件而言,VIEW 直接改 STORE 就完了。不用如此費工。但對臉書這麼大的軟件,STORE 改變很難追蹤是哪個 VIEW 變了這裡的資料。當今天出問題,只要找誰發 ACTION 就可以知道誰改進行改動。
- 推薦看 flux fb 影片。會對整個架構更了解一點。
# Redux 簡介
非常推薦官網的動畫!
資料流
- [UI]:點 deposit $10 的按鈕
- [dispatch]:dispatch 一個 action,action 事實上就是 js 的物件,他裡面會記錄你要做的事情的型態(type: 存錢)與內容(payload: $10)。
- [store]:指令進到 store 會通到一個叫 reducer 的地方,類似 array 的 reduce,reduce 有現在的狀態、現在的元素,結合之後返回一個新的狀態。我拿我現在的 state 與新進來的 action,結合後回傳新的 state,所以 state 就會改變
- 最後再回傳到 UI 上面去,所以 UI 就會再更新。
redux 重要元素:dispatch, action, store, reducer
useReaducer 類似 Redux 的 hook。
改變東西只有一條路:dispatch 一個 action,透過 reducer 回傳新的 state
Redux 基本操作
react 將 state 放到 component 裡面,redux 將狀態放到 store。store 是 js 的物件。react 與 redux 是兩個不同的概念。
創建 redux store
step1:import redux
step2:write reducer。第一個參數是 state、第二個參數是 action。記得做 state 初始化設定。reducer 這個 function 裡面會對 action 進行不同操作。記住,reducer 決定狀態變化。
step3:建立 store,透過 createStore 這個 function,將我建立好的 reducer 做完參數傳入
const { createStore } = require('redux') // node.js 舊版本無法使用 import 所以我們用 require
// 進行初始狀態設定
const initialState = {
value: 0
}
// reducer 決定狀態變化
function counterReducer(state = initialState, action) {
// 針對 action 做出不同操作
return state // 做甚麼都不變
}
let store = createStore(counterReducer)
console.log(store) // 執行 code => 結果是一串包含很多的 obj
console.log(store.getState()) // { value: 0 }
{
dispatch: [Function: dispatch],
subscribe: [Function: subscribe],
getState: [Function: getState], // 呼叫 getState 可顯示現在的 state
replaceReducer: [Function: replaceReducer],
'@@observable': [Function: observable]
}
改變 state
dispatch 一個操作,reducer 就會收到,所以 dispatch 時 action 就會到 reducer 這邊
reducer 可以根據傳來的 action 去決定回傳甚麼樣的 state。
const { createStore } = require('redux')
const initialState = {
value: 0
}
function counterReducer(state = initialState, action) {
console.log('received action', action)
return state
}
let store = createStore(counterReducer)
store.dispatch({
type: 'plus' // 慣例是使用 type
})
received action { type: '@@redux/INIT1.1.e.4.9.a' } // redux 建立 store 所初始化的 action
received action { type: 'plus' } // 印出 dispatch 的 action
const { createStore } = require('redux')
const initialState = {
value: 0
}
function counterReducer(state = initialState, action) {
console.log('received action', action)
if (action.type === 'plus') {
return {
value: state.value + 1 // 回傳新的 state
}
}
return state
}
let store = createStore(counterReducer)
console.log('first state', store.getState())
store.dispatch({
type: 'plus'
})
console.log('second state', store.getState())
// 印出的結果如下:
received action { type: '@@redux/INITf.c.d.g.y' }
first state { value: 0 }
received action { type: 'plus' }
second state { value: 1 }
以此類推 ...
const { createStore } = require('redux')
const initialState = {
value: 0
}
function counterReducer(state = initialState, action) {
console.log('received action', action)
if (action.type === 'plus') {
return {
value: state.value + 1
}
} else if (action.type === 'minus') {
return {
value: state.value - 1
}
}
return state
}
let store = createStore(counterReducer)
console.log('first state', store.getState())
store.dispatch({
type: 'plus'
})
store.dispatch({
type: 'plus'
})
console.log('second state', store.getState())
store.dispatch({
type: 'minus'
})
console.log('third state', store.getState())
// 印出的結果如下
received action { type: '@@redux/INITz.4.n.3.g.q' }
first state { value: 0 }
received action { type: 'plus' }
received action { type: 'plus' }
second state { value: 2 }
received action { type: 'minus' }
third state { value: 1 }
當 type 變的越來越多,比較適合用 switch case
function counterReducer(state = initialState, action) {
console.log('received action', action)
switch(action.type) {
case 'plus' : {
return {
value: state.value + 1
}
}
case 'minus' : {
return {
value: state.value - 1
}
}
default : { // 不在預期 state 裡面
return state
}
}
}
訂閱 store 改變,要做的事
subscribe 可以想成是 listener funct
使用 store.subscribe(這邊傳 funct)
,表示當 store 有改變,就會觸發我傳進去的 function。
執行流程:
- dispatch 後,action 會到 reducer
- reducer 回傳新的 state
- state 改變後觸發 subscribe 內的 funct。
function counterReducer(state = initialState, action) {
console.log('received action', action)
switch(action.type) {
case 'plus' : {
return {
value: state.value + 1
}
}
case 'minus' : {
return {
value: state.value - 1
}
}
default : {
return state
}
}
}
let store = createStore(counterReducer)
store.subscribe(() => {
console.log('changed!', store.getState())
})
store.dispatch({type: 'plus'})
Ex todo-list
action 是 js 的物件,用途為表示想做甚麼樣的操作,可以自行定義。
- type
- payload (你想傳的參數)
const { createStore } = require('redux')
const initialState = {
todos: []
}
function counterReducer(state = initialState, action) {
console.log('received action', action)
switch(action.type) {
case 'add_todo' : {
return {
// 同 react 是 immutable
todos: [...state.todos, {
name: action.payload.name
}]
}
}
default : {
return state
}
}
}
let store = createStore(counterReducer)
store.subscribe(() => {
console.log('changed!', store.getState())
})
store.dispatch({
type: 'add_todo',
payload: {
name: 'i am new todo'
}
})
// console 的結果
received action { type: '@@redux/INITv.z.b.a.k.d' }
received action { type: 'add_todo', payload: { name: 'i am new todo' } }
changed! { value: [ { name: 'i am new todo' } ] }
實際開發會有更多 state , 所以上述寫法會出問題。因為 return 的東西會取代原本的東西變成一個全新的 state。解決方法,透過解構將之前的狀態保存,只針對要修改的部分去做操作
const initialState = {
email: '123@gmail.com',
todos: []
}
//結果 email 不見囉
changed! { value: [ { name: 'i am new todo' } ] }
// 解決方式
switch(action.type) {
case 'add_todo' : {
return { // 同 react immutable 所以不能直接改
...state, // 將之前的 state 保存起來,只改我要改的
todos: [...state.todos, {
name: action.payload.name
}]
}
}
幫每個 todo 加上 id
const { createStore } = require('redux')
const initialState = {
email: '123@gmail.com',
todos: []
}
let todoId = 0
function counterReducer(state = initialState, action) {
console.log('received action', action)
switch(action.type) {
case 'add_todo' : {
return {
...state,
todos: [...state.todos, {
id: todoId++,
name: action.payload.name
}]
}
}
default : {
return state
}
}
}
let store = createStore(counterReducer)
store.subscribe(() => {
console.log('changed!', store.getState())
})
store.dispatch({
type: 'add_todo',
payload: {
name: 'todo0'
}
})
store.dispatch({
type: 'add_todo',
payload: {
name: 'todo1'
}
})
實作刪除功能
const { createStore } = require('redux')
const initialState = {
email: '123@gmail.com',
todos: []
}
let todoId = 0
function counterReducer(state = initialState, action) {
console.log('received action', action)
switch(action.type) {
case 'add_todo' : {
return {
...state,
todos: [...state.todos, {
id: todoId++,
name: action.payload.name
}]
}
}
case 'delete_todo' : {
return {
...state,
todos: state.todos
.filter(todo => todo.id !== action.payload.id)
}
}
default : {
return state
}
}
}
let store = createStore(counterReducer)
store.subscribe(() => {
console.log('changed!', store.getState())
})
store.dispatch({
type: 'add_todo',
payload: {
name: 'todo0'
}
})
store.dispatch({
type: 'add_todo',
payload: {
name: 'todo1'
}
})
// 刪除 id 是 0 的 todo
store.dispatch({
type: 'delete_todo',
payload: {
id: 0
}
})
reducer 優點:
- 好寫測試
- reducer is a pure function,不會在裡面 call api、寫 localStorage,只會做的就是回傳一個 state
// 優點一、好寫測試
expect(counterReducer(initialState, {
type: 'add-todo',
payload: {
name: '123'
}
})).toEqual({
todos: [{name: '123'}]
})
打錯字的好幫手:action constants
當 todos 越變越多如果打錯字怎麼辦 ? add-todo 打成 addd-todo ? 所以有了 action constants
若打錯字,redux 會報錯告知 ActionTypes 沒有這個屬性,是不是拼錯了
若使用字串方式,打錯字不會有任何的發現,因為你還是傳了一個 type 出來,只是沒有處理,對沒有處理的 type,reducer 不會做任何報錯告知
const { createStore } = require('redux')
// action constants
const ActionTypes = {
ADD_TODO: 'add_todo',
DELETE_TODO: 'delete_todo'
}
const initialState = {
email: '123@gmail.com',
todos: []
}
let todoId = 0
function counterReducer(state = initialState, action) {
console.log('received action', action)
switch(action.type) {
case ActionTypes.ADD_TODO: {
return { // 同 react immutable 所以不能直接改
...state, // 將之前的 state 保存起來,只改我要改的
todos: [...state.todos, {
id: todoId++,
name: action.payload.name
}]
}
}
case ActionTypes.DELETE_TODO : {
return {
...state,
todos: state.todos
.filter(todo => todo.id !== action.payload.id)
}
}
default : {
return state
}
}
}
let store = createStore(counterReducer)
store.subscribe(() => {
console.log('changed!', store.getState())
})
store.dispatch({
type: ActionTypes.ADD_TODO,
payload: {
name: 'todo0'
}
})
store.dispatch({
type: ActionTypes.ADD_TODO,
payload: {
name: 'todo1'
}
})
store.dispatch({
type: ActionTypes.DELETE_TODO,
payload: {
id: 0
}
})
action creator
解決問題 :每次都要打 type 與 payload 超麻煩
所以可以將重複的部份包成 function,透過呼叫 function,回傳 action
function addTodo(name) {
return {
type: ActionTypes.ADD_TODO,
payload: {
name
}
}
}
function deleteTodo(id) {
return {
type: ActionTypes.DELETE_TODO,
payload: {
id
}
}
}
store.dispatch(addTodo('todo0'))
store.dispatch(addTodo('todo1'))
store.dispatch(deleteTodo(0))
action creator 與 action types 都只是幫忙我們開發上比較少錯誤!
- Redux 有提供 library 可以傳多個 reducer。比方說 todo 是一個 reducer、user 又是一個 reudcer,藉由不同 reducer 處理不同 action。
React-redux:套上 React
recall redux 基本概念:
reducer
- 建立 reducer 這個 function,透過接收 state 與 action 回傳新的 state ( ie
newState = reducer(currentState, action)
)
dispatch
- dispatch action 會進來 reducer 這邊,透過接收現在的 state 與傳進來的 action 回傳新的 state
- dispatch type 與 payload 會有 typeError => 解決方法:宣告 const ActionTypes;typeAnnoying => 解決方法:包成 function,透過 action creator 只要 call function 他就會回傳給我相對應的 js 物件
store
- store.getState(),將狀態取出
react-redux 介紹
目前官方推薦使用 redux-toolkit。但工作碰到的 library 有可能沒那麼新,所以還是有必要知道他是甚麼。底層是相同,但提供的 functino 不太一樣。
step1:src 建立資料夾 redux,將 redux 相關的都放在裡面
step2:redux 資料夾中建立 store.js。import reducer 透過 createStore() 將 reducer 傳入
import { createStore } from "redux";
import rootReducer from "./reducers";
export default createStore(rootReducer);
step3:建立 reducers 的資料夾,裡面建立各別的 reducer,如:todos.js, users.js;同一層建立 index.js 目的是結合 reducer 們,透過地工具為combineReducers()
// export default reducer funct
import { ADD_TODO, DELETE_TODO } from "../actionTypes";
const initialState = {
todos: [],
};
let todoId = 0;
export default function todoReducer(state = initialState, action) {
console.log("received action", action);
switch (action.type) {
case ADD_TODO: {
return {
// 同 react immutable 所以不能直接改
...state, // 將之前的 state 保存起來,只改我要改的
todos: [
...state.todos,
{
id: todoId++,
name: action.payload.name,
},
],
};
}
case DELETE_TODO: {
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload.id),
};
}
default: {
return state;
}
}
}
import { ADD_USER } from "../actionTypes";
const initialState = {
users: [],
};
export default function usersReducer(state = initialState, action) {
switch (action.type) {
case ADD_USER: {
return {
...state,
users: [
...state.users,
{
name: action.payload.name,
},
],
};
}
default: {
return state;
}
}
}
可以把 reducer 想成種類。要留意,store 產生的東西和你想的不太一樣。
import { combineReducers } from "redux";
import todos from "./todos";
import users from "./users";
export default combineReducers({
todos, // 表示 todos 這個狀態我要用 todosReducer 負責
users,
});
// store 的資料結構
{
todos: {
// todosReducer
todos: []
}
users: {
// usersReducer
users: []
}
}
也可以這樣命名
export default combineReducers({
todoState: todos,
users,
});
step4:在 redux 裡面建立 actionType.js。官方推薦的寫法不是物件。
// 原本的寫法
const ActionTypes = {
ADD_TODO: "add_todo",
DELETE_TODO: "delete_todo",
ADD_USER: "add_user"
}
// 官方寫法
export const ADD_TODO = "add_todo";
export const DELETE_TODO = "delete_todo";
export const ADD_USER = "add_user";
step5:在 redux 裡面建立 actions.js,就是前面介紹的 action creator
import { ADD_TODO, DELETE_TODO, ADD_USER } from "./actionTypes";
export function addTodo(name) {
return {
type: ADD_TODO,
payload: {
name,
},
};
}
export function deleteTodo(id) {
return {
type: DELETE_TODO,
payload: {
id,
},
};
}
export function addUser(name) {
return {
type: ADD_USER,
payload: {
name,
},
};
}
my-app/src/index.js
redux 背後是用 context 但他做了更多額外的事情,因為 context 某些時候效能沒那麼好。這樣一來 App 就可以拿到東西
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import store from "./redux/store";
import { Provider } from "react-redux";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
my-app/src/App
通常對 store 裡面的資料需要做一些轉換,redux 提供 useSelector 讓我們可以選到 state。他的本質是 function,帶的參數為 store 的 state
import React from "react";
import { useSelector } from "react-redux";
function App() {
const state = useSelector((state) => state);
console.log("state", state);
return <div>123</div>;
}
export default App;
如果在每個需要的地方各別拿出會變得太長。
function App() {
const todos = useSelector((store) => store.todoState.todos);
console.log("state", todos);
return <div>123</div>;
}
可以在 store.js 同層開一個 selectors.js
import { selectTodos } from "./redux/selectors";
function App() {
const todos = useSelector(selectTodos);
console.log("state", todos);
return <div>123</div>;
}
export const selectTodos = (store) => store.todoState.todos;
透過 useDispatch( ) 拿 dispatch
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { selectTodos } from "./redux/selectors";
import { addTodo } from "./redux/actions";
function App() {
const todos = useSelector(selectTodos);
const dispatch = useDispatch();
console.log("state", todos);
return (
<div>
<button
onClick={(e) => {
dispatch(addTodo(Math.random()));
}}
></button>
<ul>
{todos.map((todo) => (
<li>
{todo.id} {todo.name}
</li>
))}
</ul>
</div>
);
}
redux devtool 介紹
要在 DEV 上做一些設定才能存取到。git-hub 連結
在沒有 hooks 以前:connect
connect 可以將 react 的 component 跟 redux 結合在一起。
connect 是一個 funct,他會傳兩個參數:
- mapStateToProps (type-funct, 會把 state 給我,可以 return 想要拿的 state,就會變成我 component 的 props)、概念類似 selector
- mapDispatchToProps 會給我 dispatch,若 props 名稱 與 action 名稱一樣,可以直接簡化成傳物件
const mapDispatchToProps = {
addTodo,
};
等價
const connectToStore = connect(mapStateToProps, mapDispatchToProps);
const ConnectedAddTodo = connectToStore(AddTodo);
export default ConnectedAddTodo; // smart component, container
export default connect(mapStateToProps, mapDispatchToProps)(AddTodo)
透過包了很多 function 跟 redux 串接起來。這種方式就是 HOC (component 再包一層 component)
這種做法與 hooks 的作法,最大差異在於 AddTodo 不知道 redux 的存在。
import React from "react";
import { connect } from "react-redux";
import { addTodo } from "./redux/actions";
// dumb component 不知道有 redux 的存在,僅負責 ui 的顯示
function AddTodo({ addTodo }) {
return (
<button
onClick={() => {
addTodo(Math.random());
}}
>
add todo
</button>
);
}
const mapStateToProps = (store) => {
return {
todos: store.todos.todos,
};
};
const mapDispatchToProps = (dispatch) => {
return {
addTodo: (payload) => dispatch(addTodo(payload)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AddTodo);
通常會再開資料夾 containers 與 components
這種寫法方便測試,components 的測試歸 components;redux 的測試歸 redux
// components 不存取 redux 的東西
export default function AddTodo({ addTodo }) {
return (
<button
onClick={() => {
addTodo(Math.random());
}}
>
add todo
</button>
);
}
// containers 才會知道 redux 的存在
import { connect } from "react-redux";
import { addTodo } from "../redux/actions";
import AddTodo from "../components/AddTodo";
// dumb component 不知道有 redux 的存在,僅負責 ui 的顯示
const mapStateToProps = (store) => {
return {
todos: store.todos.todos,
};
};
const mapDispatchToProps = (dispatch) => {
return {
addTodo: (payload) => dispatch(addTodo(payload)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AddTodo);
也可以用 hooks 的方式實作 containers
實作簡易 todo list
只做新增、刪除功能。redux 更適合場合是 global 狀態,比方說登入紀錄。 component 沒有存取好會有效能上的隱憂。
改變 state 的方式和 context 不一樣。也可以用 context 加上 useReducer 做到相同效果,但 redux 還幫我們處理一些效能上的問題。
// AddTodo components
import { useState, Fragment } from "react";
export default function AddTodo({ addTodo }) {
const [value, setValue] = useState("");
return (
<Fragment>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<button
onClick={() => {
addTodo(value);
setValue("");
}}
>
add todo
</button>
</Fragment>
);
}
不建議 hooks 與 connect 混著寫
// app
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { selectTodos } from "./redux/selectors";
import AddTodo from "./containers/AddTodo";
import { deleteTodo } from "./redux/actions";
function App() {
const todos = useSelector(selectTodos);
const dispatch = useDispatch();
return (
<div>
<AddTodo />
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.id} {todo.name}
<button onClick={() => dispatch(deleteTodo(todo.id))}>
delete
</button>
</li>
))}
</ul>
</div>
);
}
export default App;