React+Redux App同步数据流
徐徐 抱歉选手

feature-k Slice

首先从某个feature的Slice定义开始。

src下创建文件夹

在src文件夹下新建一个features文件夹,然后再用具体的该特征feature1为名创建一个子文件夹,最后创建feature1slice.js。整个过程就是src => features => feature-k => feature-kSlice.js。

All of the code related to feature-k should go in the feature-k folder.

initialState与createSlice的export

在feature-kSlice.js中,使用createSlice API创建reducer function。reducer function需要一些initial data 传入,因为在初次加载的时候store中的数据才有会值,才方便我们做UI component。

  1. 用const和数组创建用来描述这部分数据的fake object,取名为initialState,并把它传入createSlice去。

  2. 用createSlice创建feature-kSlice变量,主要是name,initialState,reducers。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import { createSlice } from '@reduxjs/toolkit'

    const initialState = [
    { id: '1', title: 'First Post!', content: 'Hello!' },
    { id: '2', title: 'Second Post', content: 'More text' }
    ]

    const postsSlice = createSlice({
    name: 'posts',
    initialState,
    reducers: {}
    })

    export default postsSlice.reducer
  1. 在feature-kSlice.js中吧reducer function部分export出去export default postsSlice.reducer,从app/store.js中import这个文件import postsReducer from '../features/posts/postsSlice',并在configureStore中添加该reducer。


    完成以上几个步骤之后,运行这个程序并用redux devtools检查工具是可以看到state标签中给出的初始化数据initialState的。

createSlice的export分为两个部分,第一个是在定义createSlice的reducer时同步生成的action的export,第二个是整体reducer的export到App.js。因为这里最初还没有添加actions和reducers,所以没有export actions。

feature-k UI component

因为我们在这个feature中已经有了一些initial state/fake object array,因此在该feature下开始考虑UI component。由于UI component本身也分层级,我们最最顶层的postList.js开始考虑。

在import完了必要的API之后,export const UI1 = () => {}会跟一个箭头函数,这个函数内部会涉及store数据取用,html相关UI描述,最后return的内容就是整体UI模块。

使用selector从redux store获取data

为了从redux store获得data,需要使用useSelector Hook。useSelector Hook会使用Provider包裹起来的所有组件部分都能够访问到作为参数传入进去的全局数据store。在UI组件中调用他,只会返回被该组件需要的特定数据。

The “selector functions” that you write will be called with the entire Redux state object as a parameter, and should return the specific data that this component needs from the store.

写UI

在获取了数据之后就可以使用map等循环语句开始制作html相关代码块。当最终需要的代码块由多个子模块组成的时候,可以先const定义多个子模块,最后在return()内部组装这些模块。

在顶层APP添加UI

在结束添加特定UI之后,需要在顶层App.js文件中import该UI,并且修改路由,这个部份涉及路由问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {
BrowserRouter as Router,
Switch,
Route,
Redirect
} from 'react-router-dom'

import { PostsList } from './features/posts/PostsList'

function App(){
<Router>
<div>
<Switch>
<Route
exact
path="/"
render={() => (
<React.Fragment>
<PostsList />
</React.Fragment>)}
/>
</Switch>
</div>
</Router>
}

以上涉及的知识静态UI的创建,他的数据都是放在store里面的,并且目前为止reducer是空的。

如果浏览器客户端产生了动态数据该如何处理?比如我们要在原来的postList的UI之上创建一个AddPostForm的UI,这个UI会因为用户的交互产生数据呢?如何做到UI compoennt一方的数据和store一方的数据保持一致呢?

UI component内部数据管控

这里所说的UI component内部数据管控是什么意思呢?这是因为当用户在客户端浏览器对UI进行输入,修改,点击等操作的时候,作用的对象都是UI component。UI component内部应当有管理这些数据的函数,这就需要用到React Hook中的useState API。在UI component中首先需要启用useState()产生data1和setdata1函数。

每一次绑定了data1数据的UI发生变更的时候,放在UI component的onClick/onChange属性中的监听函数就会发现这个变更,调用回调函数ondata1Changed,而回调函数函数本身就是由useState hook的setdata1构成。这个监听函数就被放在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 import React, { useState } from 'react'

export const AddPostForm = () => {
const [title, setTitle] = useState('')

const onTitleChanged = e => setTitle(e.target.value)

return (
<section>
<h2>Add a New Post</h2>
<form>
<label htmlFor="postTitle">Post Title:</label>
<input
type="text"
id="postTitle"
name="postTitle"
value={title}
onChange={onTitleChanged}
/>
<button type="button">Save Post</button>
</form>
</section>
)
}

过程概述为:

用户对input UI进行输入

=> onChange事件监听函数检测到input change

=> 调用该事件监听函数的回调函数

=> 回调函数内部通过e.target/event target对象读取UI的value,配合useState在函数UI组件内部调用setdata1,最终完成data1的更新。

UI component与store通信

在完成UI组件内部的数据用useState Hook更新储存之后,这些数据要传递到store。有可能UI组件内部储存的数据无条件的同步到store,也有可能用户的某个行为,如保存/确认/发送,出发了UI组件与store的同步。

需要注意的是useState总是动态追踪变化,比如一个input的内容,在用户输入的过程中,useState也会更随着更新;而将数据同步到store的都是最终数据,global的数据,the latest values for the input fields。

useDispatch实现UI向store传递数据

为了实现UI component和store的通信,首先在方法上需要添加useDispatch()方法,以及store中包含的某个reducer。该reducer作为参数传入useDispatch()方法,而该reducer的参数就是UI component需要传递到store的数据,至于传入是多个参数还是一个payload对象需要看creatSlice中reducer的定义方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
export const AddPostForm = () => {
const dispatch = useDispatch()
const onSavePostClicked = () => {
if (title && content) {
dispatch(
postAdded({
id: nanoid(),
title,
content
})
// 这里传入的顺序就是createSLice方法中reducer的payload
)

setTitle('')
setContent('')
}
}
return (
<section>
<h2>Add a New Post</h2>
<form>
{/* omit form inputs */}
<button type="button" onClick={onSavePostClicked}>
Save Post
</button>
</form>
</section>
)
}

调用createSlice中actionCreator的postAdded传入的是一个对象,这个对象中包含了UI component利用useState存放的数据。actionCreator内部把这个对象用action.payload来引用。

1
2
3
4
5
6
7
8
9
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded(state, action) {
state.push(action.payload)
}
}
})

action payload的两种写法

默认在reducer中使用action.payload

以上两个代码块的定义与调用是基于默认的把reducer在UI中作为{}包裹的对象传递到slice中去,在slice的reducer中使用action.payload访问。操作的形式都是对象。

手写action creator与payload自定义

Right now, we’re generating the ID and creating the payload object in our React component, and passing the payload object into postAdded.


手写actioncreators的一个好处就是可以自定义action的payload。需要注意的是这个id的生成不能在reducer中去生成,不然数据流都是不可预测的。在上面的例子中,id的随机生成是在UI component中完成的。在下面的例子中因为手写了action creator所以能够自定义payload。

If an action needs to contain a unique ID or some other random value, always generate that first and put it in the action object. Reducers should never calculate random values, because that makes the results unpredictable.

1
2
3
4
5
6
7
8
// hand-written action creator
function postAdded(title, content) {
const id = nanoid()
return {
type: 'posts/postAdded',
payload: { id, title, content }
}
}

虽然createSlice只要写了reducer就能自动生成action creator,但是我们如何在给自动生成的action creator定义我们想要的payload呢?玩意需要传递的数据十分复杂呢?

createSlice中自定义payload的prepare callback function

createSlice允许我们在reducer对象的某个特定reducer中给该reducer和action creator定义payload,只需要在定义reducer的同时定义prepare callback function即可,即define a “prepare callback” function when we write a reducer。

prepare函数的定义时传入的参数就和手写action creator传入的参数是一样的,都是从UI component处获得的。该函数返回一个payload filed对象,里面就是该reducer与action creator的payload。

The “prepare callback” function can take multiple arguments, generate random values like unique IDs, and run whatever other synchronous logic is needed to decide what values go into the action object. It should then return an object with the payload field inside.

当我们在UI component中调用这个action creator的时候,传入reducer函数的参数就是多个组成payload的元素,而不再是一个对象。

两种定义方式的比较

默认action.payload

createSlice定义reducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded: {
reducer(state, action) {
state.push(action.payload)
},
prepare(title, content, userId) {
return {
payload: {
id: nanoid(),
title,
content,
user: userId
}
}
}
}
// other reducers
}
})

UI组件调用reducer

1
2
3
4
5
6
7
const onSavePostClicked = () => {
if (title && content) {
dispatch(postAdded(title, content, userId))
setTitle('')
setContent('')
}
}

prepare callbackfunction

createSlice定义reducer

1
2
3
4
5
6
7
8
9
// 这部分内容包裹在postSlice中
postUpdated(state, action){
const { id, title, content } = action.payload
const existingPost = state.find(post => post.id === id)
if(existingPost){
existingPost.title = title
existingPost.content = content
}
},

UI组件调用reducer

1
2
3
4
5
6
const onSavePostClicked = () => {
if(title && content){
dispatch(postUpdated({id: postId, title, content}))
history.push(`/posts/${postId}`)
}
}

多个单页UI component与路由

在完成了某个具备单独成页性质的UI component之后,需要在顶层文件App.js中将它import并使用Route API添加。

其次需要查看页面间路由,比如从列表页到详情页,需要在列表页添加到详情页的Link API。感觉Link API就有点像href。

  • 本文标题:React+Redux App同步数据流
  • 本文作者:徐徐
  • 创建时间:2020-12-06 19:45:24
  • 本文链接:https://machacroissant.github.io/2020/12/06/redux-react-app-example/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论