Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从消息队列的角度来理解 Redux #9165

Open
guevara opened this issue Dec 27, 2022 · 0 comments
Open

从消息队列的角度来理解 Redux #9165

guevara opened this issue Dec 27, 2022 · 0 comments

Comments

@guevara
Copy link
Owner

guevara commented Dec 27, 2022

从消息队列的角度来理解 Redux



https://ift.tt/Qa8M21f






在 React 中,组件之间是不能互相通信的,数据只能自上而下流动。所以必须把状态放在最高层的 组件中。复杂一点的页面肯定会导致状态越提越高,所以最终还是需要一个单独的状态存储——redux。

Redux 的文档是非常差的。它只写了 What 和 How, 而没有写 Why. 他只说了自己是个状态存储,却 不说为什么需要其他的东西,这就让初学者感到非常地迷惑,实际上它只要提到一下 "Event Bus" 这个词就非常容易理解了,非要上来就讲什么 Action/Store/Reducer/SingleSourceOfTruth 之类的。 根据最高指示:代码是用来读的,不是用来装逼的,俗称 "码读不装", 那么我们这里先来补上 redux 的 Why.

Why Redux?

Redux 本质上来说有很大一部分功能就是一个 Event Bus, 或者 Message Queue, 又或者 Pub/Sub. React 组件之间需要通信,如果让他们之间互相访问,那么就是一个 O(N^2) 的复杂度,而如果让他们通过一个中间组件,也就是 Redux 通信,那么复杂度就大大降低了,更重要的是,代码更清晰了。当然,Redux 不只是负责他们之间的通信,而且是把状态存储了下来。这两篇 文章 写得非常好了。

What is Redux?

Redux 里的概念特别多,不过知道了 EventBus, 就都非常好理解了,这里先列出来:

  • store, 存放状态的唯一容器,类似于消息队列的 broker.
  • reducer, 就是用来处理消息的函数,其实一般叫做 handler, 起个 reducer 的名字好像就函数式了,瞬间高大上加逼格高了起来。这个函数签名是 function(state, action) -> new_state.
  • action 和 dispatch, Action 其实就是消息总线里面的消息或者说事件,分发 Action, 其实不就是生产者么?
  • actionCreator, actionCreator 就是用来创建 Action 的。Action 不过就是一个 {type, value} 的字典罢了。function xxxCreator() -> action 就叫做一个 actionCreator
  • Middleware, 其实就是字面意思,会处理你的状态的一些中间件。
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

在上面的例子中,实际上完全没有涉及到 react 的相关内容,redux 本身就是可以独立存在的一个库。但是,99.99% 的情况下,你还是为了管理 react 的状态采用 redux 的。要在 react 中使用 redux , 需要使用 react-redux 库。

这里需要注意的是,都 2020 年了,我们自然要用 hooks 了,所以网上好多还在讲 connect 的教程可以不用看了。几个常用的钩子:

  • useSelector, 顾名思义,用来从 store 中选取需要的状态,相当于消费者 consume
  • useDispatch, 用来 dispatch action, 相当于生产者 produce
  • useStore, 虽然有这个 hook, 但是文档里明确说了不推荐用

import {useSelector, useDispatch} from "react-redux";

const result = useSelector((state) => selectedState)

const dispatch = useDispatch()

<button onClick={() => dispatch({ type: 'increment-counter' })}>
Increment counter
</button>

不使用 hooks 的方式

在没有 hooks 之前,需要使用 react-redux 库提供的 connect/mapState/mapDispatch 几个函数来实现 redux 和 react 组件之间的交互。

  • useSelector 对应以前的 mapState 函数
  • useDispatch 对应以前的 mapDispatch 函数

设计 redux state tree

我们知道 redux 的 state 是作为一个树存在的,设计这个树的形态是 redux 使用的重中之重了。

第一个问题,是否需要把所有的状态放进 redux 呢?不一定,对于一些组件内部的 UI 状态,比如是否隐藏某个按钮,是可以使用 useState 放在内部的。重点在于:你这个状态其他组件关心吗?一般来说,表单的状态时不需要放进 redux 的。

第二个问题,对于多个页面,比如说:一个列表页,一个详情页,一个用户页,要把所有的状态放在一个 store 中么?我倾向于把同一个组的页面的数据放在同一个 State 里面,这里的组大概就相当于 Django 的 App 或者是 Flask 的 Blueprint 的概念。当然,对于小型项目来说,用一个 store 就够了。

总的来说,分以下几步:

  1. 确定全局状态,比如用户登录状态,放在 redux 最顶级。
  2. 设计每个页面分区可视化树。根据页面布局,确定组件,以及每个组件的状态。
  3. 设计 reducers, 同样是一棵树状的组织。
  4. 实现 actions, 其中也包括加载数据的 action.
  5. 实现展示层,也就是组件们。

页面的布局图和状态树:

使用 normalized data

当我们使用 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: '.....'
      }
    ]
  }

]

更好的组织方式

{
    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 : {....}
    }
}

何时发送数据请求?

通过 side effects 和 thunk 与服务器通信

从上面的脚本我们可以看出,redux 完全是一个本地的消息处理,然而当我们在本地做出更改的时候,肯定需要在放到服务器啊,这种操作在 redux 中被称作 side effects(副作用), 可以使用 redux-thunk 来实现。


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) => {    
<span>dispatch</span>({      
  <span>type</span>: <span>FETCH_SEARCH_BEGIN</span>    
});
<span>try</span> {      
  
  <span>const</span> result = <span>await</span> <span>fetch</span>(
    <span>"api/search/?q=xxx"</span>,
    args.<span>pageCount</span>, 
    args.<span>itemsPerPage</span>
  );           
  
  <span>dispatch</span>({        
    <span>type</span>: <span>FETCH_SEARCH_SUCCESS</span>,        
    <span>payload</span>: result,        
    <span>currentPage</span>: args.<span>pageCount</span>      
  });    
} <span>catch</span> (err) {     
  
  <span>dispatch</span>({
    <span>type</span>: <span>FETCH_SEARCH_FAILURE</span>,        
    <span>error</span>: 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 的这个钩子来实现。

redux 项目目录结构

只要分着放 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 一起使用

当我们和 react-router 一起使用的时候,会遇到一个问题,组件是从 URL 中读取状态,还是从 store 中读取状态呢?这两个状态之间怎么同步呢?redux 的作者 Dan 给出了答案:

不要在 redux 中保存 URL 中的状态,直接从 URL 中读取。可以把 URL 想象成另外一个单独的小的 Store.

遗留问题

如何增加一个新的 Action

  • 在 actions 中增加对应的 fetch action
  • 在 actionTypes 中增加对应的 type
  • 在 actions 中增加对应的 initState 和 reducer
  • 在 rootReducer 中增加对应的 reducer
  • 对应的组件中增加 useEffect 的 dispatch 事件
  • 对应的组件中增加 useSelector 使用数据

参考

  1. Redux 的本质,Event bus
  2. https://stackoverflow.com/questions/50703862/react-redux-state-shape-for-multiple-pages
  3. https://stackoverflow.com/questions/37288070/designing-redux-state-tree
  4. https://stackoverflow.com/questions/33619775/redux-multiple-stores-why-not
  5. Redux 中文基础教程
  6. https://www.pluralsight.com/guides/how-to-structure-redux-components-and-containers
  7. https://www.pluralsight.com/guides/how-to-organize-your-react-+-redux-codebase
  8. 使用 hooks 编写 redux
  9. https://medium.com/@gaurav5430/async-await-with-redux-thunk-fff59d7be093
  10. https://medium.com/fullstack-academy/thunks-in-redux-the-basics-85e538a3fe60
  11. The best way to architect your Redux app
  12. Where and When to Fetch Data With Redux
  13. https://reacttraining.com/blog/useEffect-is-not-the-new-componentDidMount/
  14. How to sync Redux state and url query params
  15. https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape

如何加载数据

  1. https://stackoverflow.com/questions/51113369/how-to-dispatch-fetched-data-from-api
  2. https://stackoverflow.com/questions/39419237/what-is-mapdispatchtoprops
  3. You have to include dispatch in useEffect because of lint rules
  4. https://stackoverflow.com/questions/38206477/react-router-redux-fetch-resources-if-needed-on-child-routes
  5. https://www.freecodecamp.org/news/loading-data-in-react-redux-thunk-redux-saga-suspense-hooks-666b21da1569/
  6. https://stackoverflow.com/questions/57097390/react-hooks-fetch-data-inside-useeffect-warning
  7. https://stackoverflow.com/questions/57925027/useeffect-goes-in-infinite-loop-when-combined-usedispatch-useselector-of-redux
  8. https://stackoverflow.com/questions/62167174/need-clarification-for-react-react-redux-hooks-middleware-thunk-fetching-ap

其他的状态管理

  1. https://github.com/jamiebuilds/unstated-next
  2. https://imweb.io/topic/5a453691a192c3b460fce36e
  3. https://news.ycombinator.com/item?id=12371248
  4. Understanding MobX and when to use it. mobxjs/mobx#199
  5. https://blog.rocketinsights.com/redux-vs-mobx/






via Yifei's Notes

December 27, 2022 at 02:34PM
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant