Posted on:
Last modified:
在 React 中,组件之间是不能互相通信的,数据只能自上而下流动。所以必须把状态放在最高层的 组件中。复杂一点的页面肯定会导致状态越提越高,所以最终还是需要一个单独的状态存储——redux。
Redux 的文档是非常差的。它只写了 What 和 How, 而没有写 Why. 他只说了自己是个状态存储,却 不说为什么需要其他的东西,这就让初学者感到非常地迷惑,实际上它只要提到一下 "Event Bus" 这个词就非常容易理解了,非要上来就讲什么 Action/Store/Reducer/SingleSourceOfTruth 之类的。 根据最高指示:代码是用来读的,不是用来装逼的,俗称 "码读不装", 那么我们这里先来补上 redux 的 Why.
Redux 本质上来说有很大一部分功能就是一个 Event Bus, 或者 Message Queue, 又或者 Pub/Sub.
React 组件之间需要通信,如果让他们之间互相访问,那么就是一个 O(N^2)
的复杂度,而如果让
他们通过一个中间组件,也就是 Redux 通信,那么复杂度就大大降低了,更重要的是,代码更清晰了。
当然,Redux 不只是负责他们之间的通信,而且是把状态存储了下来。
Redux 里的概念特别多,不过知道了 EventBus, 就都非常好理解了,这里先列出来:
import {createStore} from "redux";
function reducer(state = {}, action) {
switch (action.type) {
case "XXX":
return {
...state,
XXX: action.value
};
case "YYY":
return {
}
default:
return state;
}
}
let store = createStore(reducer);
store.dispatch({type: "XXX", value: "XXX"})
console.log(store.getState())
以上就是一个非常基本的 redux 应用啦,如果要有多个 reducer 分开处理不同事件怎么办呢?使用 combineReducers. 需要注意的是,这样的话,每个 reducer 只会获得其中一部分数据了,比如说 userReducer 只会获得 user 部分,itemsReducer 只会处理 items 部分。
import {createStore, combineReducers} from "redux";
let reducers = combineReducers({
user: userReducer, items: itemsReducer
})
let store = createStore(reducers)
在上面的例子中,实际上完全没有涉及到 react 的相关内容,redux 本身就是可以独立存在的一个库。但是,99.99% 的情况下,你还是为了管理 react 的状态采用 redux 的。要在 react 中使用 redux , 需要使用 react-redux 库。
这里需要注意的是,都 2020 年了,我们自然要用 hooks 了,所以网上好多还在讲 connect 的教程可以不用看了。几个常用的钩子:
// yarn add react-redux
import {useSelector, useDispatch} from "react-redux";
// 这里需要注意的是,state 是整个状态树。
const result = useSelector((state) => selectedState)
// const result = useSelector((state) => state.counter)
const dispatch = useDispatch()
// 这里需要用一个匿名函数
<button onClick={() => dispatch({ type: 'increment-counter' })}>
Increment counter
</button>
在没有 hooks 之前,需要使用 react-redux 库提供的 connect/mapState/mapDispatch 几个函数来实现 redux 和 react 组件之间的交互。
我们知道 redux 的 state 是作为一个树存在的,设计这个树的形态是 redux 使用的重中之重了。
第一个问题,是否需要把所有的状态放进 redux 呢?不一定,对于一些组件内部的 UI 状态,比如是否隐藏某个按钮,是可以使用 useState 放在内部的。重点在于:你这个状态其他组件关心吗?一般来说,表单的状态时不需要放进 redux 的。
第二个问题,对于多个页面,比如说:一个列表页,一个详情页,一个用户页,要把所有的状态放在一个 store 中么?我倾向于把同一个组的页面的数据放在同一个 State 里面,这里的组大概就相当于 Django 的 App 或者是 Flask 的 Blueprint 的概念。当然,对于小型项目来说,用一个 store 就够了。
总的来说,分以下几步:
页面的布局图和状态树:
当我们使用 useSelector 的时候,需要从 state 出发,一层层地获取数据,所以数据的层级最好不要太多,避免出现 state.posts[0].comments[0]
这种冗长的表达式。另一方面,设计不好的状态树会导致好多状态重复存储,或者不好查询。为了解决这个问题,我们可以使用 Redux 官方推荐的解决方案。
不好的方案:
const blogPosts = [
{
id: 'post1',
author: { username: 'user1', name: 'User 1' },
body: '......',
comments: [
{
id: 'comment1',
author: { username: 'user2', name: 'User 2' },
comment: '.....'
},
{
id: 'comment2',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
}
]
},
{
id: 'post2',
author: { username: 'user2', name: 'User 2' },
body: '......',
comments: [
{
id: 'comment3',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
},
{
id: 'comment4',
author: { username: 'user1', name: 'User 1' },
comment: '.....'
},
{
id: 'comment5',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
}
]
}
// and repeat many times
]
更好的组织方式
{
posts : {
byId : {
"post1" : {
id : "post1",
author : "user1",
body : "......",
comments : ["comment1", "comment2"]
},
"post2" : {
id : "post2",
author : "user2",
body : "......",
comments : ["comment3", "comment4", "comment5"]
}
},
allIds : ["post1", "post2"]
},
comments : {
byId : {
"comment1" : {
id : "comment1",
author : "user2",
comment : ".....",
},
"comment2" : {
id : "comment2",
author : "user3",
comment : ".....",
},
"comment3" : {
id : "comment3",
author : "user3",
comment : ".....",
},
"comment4" : {
id : "comment4",
author : "user1",
comment : ".....",
},
"comment5" : {
id : "comment5",
author : "user3",
comment : ".....",
},
},
allIds : ["comment1", "comment2", "comment3", "commment4", "comment5"]
},
users : {
byId : {
"user1" : {
username : "user1",
name : "User 1",
},
"user2" : {
username : "user2",
name : "User 2",
},
"user3" : {
username : "user3",
name : "User 3",
}
},
allIds : ["user1", "user2", "user3"]
}
}
而这些数据要放在 entities 中:
{
simpleDomainData1: {....},
simpleDomainData2: {....},
entities : {
entityType1 : {....},
entityType2 : {....}
},
ui : {
uiSection1 : {....},
uiSection2 : {....}
}
}
从上面的脚本我们可以看出,redux 完全是一个本地的消息处理,然而当我们在本地做出更改的时候, 肯定需要在放到服务器啊,这种操作在 redux 中被称作 side effects(副作用), 可以使用 redux-thunk 来实现。
// 添加 thunk 支持,一般在 store.js 中
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
如果是一个组件的内部数据,那么没必要在 redux 中保存状态,也就是使用 useState 就好了。获取 数据直接在 useEffect 中 fetch 数据,然后 setState 就可以了。
在 useEffect 中获取数据之后触发 action, 还是通过触发一个 action 来获取数据?这个和私有状
态是一样的考虑,如果还有其他的组件会触发这个操作更新数据,那么就使用 action 最好,如果这
个数据是组件私有的,那么在 useEffect 中直接获取就好。比如说,对于页面组件,可能直接在组件
中使用 useEffect(fn, [])
加载数据就好了,可以直接调用 fetch, 也可以触发一个 action.
对于需要获取数据的操作,一般需要三个 Action, 分别是 FETCH_XXX_BEGIN/SUCCESS/FAILURE
.
如果获取数据成功,那么 SUCCESS 的 action 中就会包含数据。另外,还要有一个 RESET_XXX_DATA
的 action, 用来清除和重置。
一个典型的加载数据的 action:
export function fetchSearchData(args) {
return async (dispatch) => {
// Initiate loading state
dispatch({
type: FETCH_SEARCH_BEGIN
});
try {
// Call the API
const result = await fetch(
"api/search/?q=xxx",
args.pageCount,
args.itemsPerPage
);
// Update payload in reducer on success
dispatch({
type: FETCH_SEARCH_SUCCESS,
payload: result,
currentPage: args.pageCount
});
} catch (err) {
// Update error in reducer on failure
dispatch({
type: FETCH_SEARCH_FAILURE,
error: err
});
}
};
}
对应的 Reducer 可以这样:
const initialState = { payload: [], isLoading: false, error: {}};
export function searchReducer( state=initialState, action ) {
switch(action.type) {
case FETCH_SEARCH_BEGIN:
return {
...state,
isLoading: true
};
case FETCH_SEARCH_SUCCESS:
return {
...state,
payload: action.payload,
isLoading: false
};
case FETCH_SEARCH_FAILURE:
return {
...state,
error: action.error,
isLoading: false
};
case RESET_SEARCH_DATA:
return {
...state,
...initialState
};
default:
return state;
}
}
在 class-based React 中,一般是在 ComponentWill/DidMount 周期中调用加载数据的逻辑,我们 现在自然是使用 useEffect 的这个钩子来实现。
只要分着放 actions 和 reducers 两个目录就可以了,其实没多大要求。我一般是这样放的:
App.js
state/
actions/
reducers/
actionTypes.js
rootReducer.js
store.js
在 index.js 中还是需要
import {Provider} from 'react-redux';
import store from 'state/store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
当我们和 react-router 一起使用的时候,会遇到一个问题,组件是从 URL 中读取状态,还是从 store 中读取状态呢?这两个状态之间怎么同步呢?redux 的作者 Dan 给出了答案:
不要在 redux 中保存 URL 中的状态,直接从 URL 中读取。可以把 URL 想象成另外一个单独的小 Store.
© 2016-2022 Yifei Kong. Powered by ynotes
All contents are under the CC-BY-NC-SA license, if not otherwise specified.
Opinions expressed here are solely my own and do not express the views or opinions of my employer.
友情链接: MySQL 教程站