使用react構建大型應用,勢必會面臨狀態管理的問題,redux是經常使用的一種狀態管理庫,咱們會由於各類緣由而須要使用它。javascript
但並非全部的state都要交給redux管理,當某個狀態數據只被一個組件依賴或影響,且在切換路由再次返回到當前頁面不須要保留操做狀態時,咱們是沒有必要使用redux的,用組件內部state足以。例以下拉框的顯示與關閉。html
react應用中咱們會定義不少state,state最終也都是爲頁面展現服務的,根據數據的來源、影響的範圍大體能夠將前端state歸爲如下三類:前端
Domain data: 通常能夠理解爲從服務器端獲取的數據,好比帖子列表數據、評論數據等。它們可能被應用的多個地方用到,前端須要關注的是與後端的數據同步、提交等等。UI state: 決定當前UI如何展現的狀態,好比一個彈窗的開閉,下拉菜單是否打開,每每聚焦於某個組件內部,狀態之間能夠相互獨立,也可能多個狀態共同決定一個UI展現,這也是UI state管理的難點。java
App state: App級的狀態,例如當前是否有請求正在loading、某個聯繫人被選中、當前的路由信息等可能被多個組件共同使用到狀態。react
在使用redux的過程當中,咱們都會使用modules的方式,將咱們的reducers拆分到不一樣的文件當中,一般會遵循高內聚、方便使用的原則,按某個功能模塊、頁面來劃分。那對於某個reducer文件,如何設計state結構能更方便咱們管理數據呢,下面列出幾種常見的方式:git
這種方式大多會出如今列表的展現上,如帖子列表頁,由於後臺接口返回的數據一般與列表的展現結構基本一致,能夠直接使用。github
以下面的頁面,分爲三個section,對應開戶中、即將流失、已提交審覈三種不一樣的數據類型。
由於頁面是展現性的沒有太多的交互,因此咱們徹底能夠根據頁面UI來設計以下的結構:web
tabData: { opening: [{ userId: "6332", mobile: "1858849****", name: "test1", ... }, ...], missing: [], commit: [{ userId: "6333", mobile: "1858849****", name: "test2", ... }, ... ] }
這樣設計比較方便咱們將state映射到頁面,拉取更多數據只須要將新數據簡單contact進對應的數組便可。對於簡單頁面,這樣是可行的。數據庫
不少狀況下,處理的數據都是嵌套或互相關聯的。例如,一個羣列表,由不少羣組成,每一個羣又包含不少個用戶,一個用戶能夠加入多個不一樣的羣。這種類型的數據,咱們能夠方便用以下結構表示:redux
const Groups = [ { id: 'group1', groupName: '連線電商', groupMembers: [ { id: 'user1', name: '張三', dept: '電商部' }, { id: 'user2', name: '李四', dept: '電商部' }, ] }, { id: 'group2', groupName: '連線資管', groupMembers: [ { id: 'user1', name: '張三', dept: '電商部' }, { id: 'user3', name: '王五', dept: '電商部' }, ] } ]
這種方式,對界面展現很友好,展現羣列表,咱們只需遍歷Groups數組,展現某個羣成員列表,只需遍歷相應索引的數據Groups[index],展現某個羣成員的數據,繼續索引到對應的成員數據GroupsgroupIndex便可。
可是這種方式有一些問題:
爲了不上面的問題,咱們能夠借鑑數據庫存儲數據的方式,設計出相似的範式化的state,範式化的數據遵循下面幾個原則:
- 不一樣類型的數據,都以「數據表」的形式存儲在state中
- 「數據表」 中的每一項條目都以對象的形式存儲,對象以惟一性的ID做爲key,條目自己做爲value。
- 任何對單個條目的引用都應該根據存儲條目的 ID 來索引完成。
- 數據的順序經過ID數組表示。
上面的示例範式化以後以下:
{ groups: { byIds: { group1: { id: 'group1', groupName: '連線電商', groupMembers: ['user1', 'user2'] }, group2: { id: 'group2', groupName: '連線資管', groupMembers: ['user1', 'user3'] } }, allIds: ['group1', 'group2'] }, members: { byIds: { user1: { id: 'user1', name: '張三', dept: '電商部' }, user2: { id: 'user2', name: '李四', dept: '電商部' }, user3: { id: 'user3', name: '王五', dept: '電商部' } }, allIds: [] } }
與原來的數據相比有以下改進:
一般咱們接口返回的數據都是嵌套形式的,要將數據範式化,咱們可使用Normalizr這個庫來輔助。
固然這樣作以前咱們最好問本身,我是否須要頻繁的遍歷數據,是否須要快速的訪問某一項數據,是否須要頻繁更新同步數據。
對於這些關係數據,咱們能夠統一放到entities中進行管理,這樣root state,看起來像這樣:
{ simpleDomainData1: {....}, simpleDomainData2: {....} entities : { entityType1 : {byId: {}, allIds}, entityType2 : {....} } ui : { uiSection1 : {....}, uiSection2 : {....} } }
其實上面的entities並不夠純粹,由於其中包含了關聯關係(group裏面包含了groupMembers的信息),也包含了列表的順序信息(如每一個實體的allIds屬性)。更進一步,咱們能夠將這些信息剝離出來,讓咱們的entities更加簡單,扁平。
{ entities: { groups: { group1: { id: 'group1', groupName: '連線電商', }, group2: { id: 'group2', groupName: '連線資管', } }, members: { user1: { id: 'user1', name: '張三', dept: '電商部' }, user2: { id: 'user2', name: '李四', dept: '電商部' }, user3: { id: 'user3', name: '王五', dept: '電商部' } } }, groups: { gourpIds: ['group1', 'group2'], groupMembers: { group1: ['user1', 'user2'], group2: ['user2', 'user3'] } } }
這樣咱們在更新entity信息的時候,只需操做對應entity就能夠了,添加新的entity時則須要在對應的對象如entities[group]中添加group對象,在groups[groupIds]中添加對應的關聯關係。
enetities.js
const ADD_GROUP = 'entities/addGroup'; const UPDATE_GROUP = 'entities/updateGroup'; const ADD_MEMBER = 'entites/addMember'; const UPDATE_MEMBER = 'entites/updateMember'; export const addGroup = entity => ({ type: ADD_GROUP, payload: {[entity.id]: entity} }) export const updateGroup = entity => ({ type: UPDATE_GROUP, payload: {[entity.id]: entity} }) export const addMember = member => ({ type: ADD_MEMBER, payload: {[member.id]: member} }) export const updateMember = member => ({ type: UPDATE_MEMBER, payload: {[member.id]: member} }) _addGroup(state, action) { return state.set('groups', state.groups.merge(action.payload)); } _addMember(state, action) { return state.set('members', state.members.merge(action.payload)); } _updateGroup(state, action) { return state.set('groups', state.groups.merge(action.payload, {deep: true})); } _updateMember(state, action) { return state.set('members', state.members.merge(action.payload, {deep: true})) } const initialState = Immutable({ groups: {}, members: {} }) export default function entities(state = initialState, action) { let type = action.type; switch (type) { case ADD_GROUP: return _addGroup(state, action); case UPDATE_GROUP: return _updateGroup(state, action); case ADD_MEMBER: return _addMember(state, action); case UPDATE_MEMBER: return _updateMember(state, action); default: return state; } }
能夠看到,由於entity的結構大體相同,因此更新起來不少邏輯是差很少的,因此這裏能夠進一步提取公用函數,在payload裏面加入要更新的key值。
export const addGroup = entity => ({ type: ADD_GROUP, payload: {data: {[entity.id]: entity}, key: 'groups'} }) export const updateGroup = entity => ({ type: UPDATE_GROUP, payload: {data: {[entity.id]: entity}, key: 'groups'} }) export const addMember = member => ({ type: ADD_MEMBER, payload: {data: {[member.id]: member}, key: 'members'} }) export const updateMember = member => ({ type: UPDATE_MEMBER, payload: {data: {[member.id]: member}, key: 'members'} }) function normalAddReducer(state, action) { let payload = action.payload; if (payload && payload.key) { let {key, data} = payload; return state.set(key, state[key].merge(data)); } return state; } function normalUpdateReducer(state, action) { if (payload && payload.key) { let {key, data} = payload; return state.set(key, state[key].merge(data, {deep: true})); } } export default function entities(state = initialState, action) { let type = action.type; switch (type) { case ADD_GROUP: case ADD_MEMBER: return normalAddReducer(state, action); case UPDATE_GROUP: case UPDATE_MEMBER: return normalUpdateReducer(state, action); default: return state; } }
在請求接口時,一般會dispatch loading狀態,一般咱們會在某個接口請求的reducer裏面來處理響應的loading狀態,這會使loading邏輯處處都是。其實咱們能夠將loading狀態做爲根reducer的一部分,單獨管理,這樣就能夠複用響應的邏輯。
const SET_LOADING = 'SET_LOADING'; export const LOADINGMAP = { groupsLoading: 'groupsLoading', memberLoading: 'memberLoading' } const initialLoadingState = Immutable({ [LOADINGMAP.groupsLoading]: false, [LOADINGMAP.memberLoading]: false, }); const loadingReducer = (state = initialLoadingState, action) => { const { type, payload } = action; if (type === SET_LOADING) { return state.set(key, payload.loading); } else { return state; } } const setLoading = (scope, loading) => { return { type: SET_LOADING, payload: { key: scope, loading, }, }; } // 使用的時候 store.dispatch(setLoading(LOADINGMAP.groupsLoading, true));
這樣當須要添加新的loading狀態的時候,只須要在LOADINGMAP和initialLoadingState添加相應的loading type便可。
也能夠參考dva的實現方式,它也是將loading存儲在根reducer,而且是根據model的namespace做爲區分,
它方便的地方在於將更新loading狀態的邏輯被提取到plugin中,用戶不須要手動編寫更新loading的邏輯,只須要在用到時候使用state便可。plugin的代碼也很簡單,就是在鉤子函數中攔截反作用。
function onEffect(effect, { put }, model, actionType) { const { namespace } = model; return function*(...args) { yield put({ type: SHOW, payload: { namespace, actionType } }); yield effect(...args); yield put({ type: HIDE, payload: { namespace, actionType } }); }; }
對於web端應用,咱們沒法控制用戶的操做路徑,極可能用戶在直接訪問某個頁面的時候,咱們store中並無準備好數據,這可能會致使一些問題,因此有人建議以page爲單位劃分store,捨棄掉部分多頁面共享state的好處,具體能夠參考這篇文章,其中提到在視圖之間共享state要謹慎,其實這也反映出咱們在思考是否要共享某個state時,思考以下幾個問題:
https://www.zhihu.com/questio...
https://segmentfault.com/a/11...
https://hackernoon.com/shape-...
https://medium.com/@dan_abram...
https://medium.com/@fastphras...
https://juejin.im/post/59a16e...
http://cn.redux.js.org/docs/r...
https://redux.js.org/recipes/...