如何設計redux state結構

爲何使用redux

使用react構建大型應用,勢必會面臨狀態管理的問題,redux是經常使用的一種狀態管理庫,咱們會由於各類緣由而須要使用它。javascript

  1. 不一樣的組件可能會使用相同的數據,使用redux能更好的複用數據和保持數據的同步
  2. react中子組件訪問父組件的數據只能經過props層層傳遞,使用redux能夠輕鬆的訪問到想要的數據
  3. 全局的state能夠很容易的進行數據持久化,方便下次啓動app時得到初始state
  4. dev tools提供狀態快照回溯的功能,方便問題的排查

但並非全部的state都要交給redux管理,當某個狀態數據只被一個組件依賴或影響,且在切換路由再次返回到當前頁面不須要保留操做狀態時,咱們是沒有必要使用redux的,用組件內部state足以。例以下拉框的顯示與關閉。html

常見的狀態類型

react應用中咱們會定義不少state,state最終也都是爲頁面展現服務的,根據數據的來源、影響的範圍大體能夠將前端state歸爲如下三類:前端

Domain data: 通常能夠理解爲從服務器端獲取的數據,好比帖子列表數據、評論數據等。它們可能被應用的多個地方用到,前端須要關注的是與後端的數據同步、提交等等。

UI state: 決定當前UI如何展現的狀態,好比一個彈窗的開閉,下拉菜單是否打開,每每聚焦於某個組件內部,狀態之間能夠相互獨立,也可能多個狀態共同決定一個UI展現,這也是UI state管理的難點。java

App state: App級的狀態,例如當前是否有請求正在loading、某個聯繫人被選中、當前的路由信息等可能被多個組件共同使用到狀態。react

如何設計state結構

在使用redux的過程當中,咱們都會使用modules的方式,將咱們的reducers拆分到不一樣的文件當中,一般會遵循高內聚、方便使用的原則,按某個功能模塊、頁面來劃分。那對於某個reducer文件,如何設計state結構能更方便咱們管理數據呢,下面列出幾種常見的方式:git

1.將api返回的數據直接放入state

這種方式大多會出如今列表的展現上,如帖子列表頁,由於後臺接口返回的數據一般與列表的展現結構基本一致,能夠直接使用。github

2.以頁面UI來設計state結構

以下面的頁面,分爲三個section,對應開戶中、即將流失、已提交審覈三種不一樣的數據類型。
示例
由於頁面是展現性的沒有太多的交互,因此咱們徹底能夠根據頁面UI來設計以下的結構:web

tabData: {
    opening: [{
        userId: "6332",
        mobile: "1858849****",
        name: "test1",
        ...
    }, ...],
    missing: [],
    commit: [{
        userId: "6333",
        mobile: "1858849****",
        name: "test2",
        ...
    }, ... ]
}

這樣設計比較方便咱們將state映射到頁面,拉取更多數據只須要將新數據簡單contact進對應的數組便可。對於簡單頁面,這樣是可行的。數據庫

3.State範式化(normalize)

不少狀況下,處理的數據都是嵌套或互相關聯的。例如,一個羣列表,由不少羣組成,每一個羣又包含不少個用戶,一個用戶能夠加入多個不一樣的羣。這種類型的數據,咱們能夠方便用以下結構表示: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便可。
可是這種方式有一些問題:

  1. 存在不少重複數據,當某個羣成員信息更新的時候,想要在不一樣的羣之間進行同步比較麻煩。
  2. 嵌套過深,致使reducer邏輯複雜,修改深層的屬性會致使代碼臃腫,空指針的問題
  3. redux中須要遵循不可變動新模式,更新屬性每每須要更新組件樹的祖先,產生新的引用,這會致使跟修改數據無關的組件也要從新render。

爲了不上面的問題,咱們能夠借鑑數據庫存儲數據的方式,設計出相似的範式化的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: []
    }
}

與原來的數據相比有以下改進:

  1. 由於數據是扁平的,且只被定義在一個地方,更方便數據更新
  2. 檢索或者更新給定數據項的邏輯變得簡單與一致。給定一個數據項的 type 和 ID,沒必要嵌套引用其餘對象而是經過幾個簡單的步驟就能查找到它。
  3. 每一個數據類型都是惟一的,像用戶信息這樣的更新僅僅須要狀態樹中 「members > byId > user」 這部分的複製。這也就意味着在 UI 中只有數據發生變化的一部分纔會發生更新。與以前的不一樣的是,以前嵌套形式的結構須要更新整個 groupMembers數組,以及整個 groups數組。這樣就會讓沒必要要的組件也再次從新渲染。

一般咱們接口返回的數據都是嵌套形式的,要將數據範式化,咱們可使用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;
  }
}

將loading狀態抽離到根reducer中,統一管理

在請求接口時,一般會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做爲區分,

dva loading

它方便的地方在於將更新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時,思考以下幾個問題:

  1. 有多少頁面會使用到該數據
  2. 每一個頁面是否須要單獨的數據副本
  3. 改動數據的頻率怎麼樣

參考文章

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/...

相關文章
相關標籤/搜索