各种使用技巧
文章目录

Redux: 各种使用技巧

缩减样板代码

Actions

Actions 是用来描述在 app 中发生了什么的普通对象。很重要的一点是 不得不 dispatch 的 action 对象并非是一个样板代码, 而是 Redux 的一个 基本设计选择.

Action 一般长这样:

1
2
3
{ type: 'ADD_TODO', text: 'Use Redux' }
{ type: 'REMOVE_TODO', id: 42 }
{ type: 'LOAD_ARTICLE', response: { ... } }

一个约定俗成的做法是, action 拥有一个不变的 type 帮助 reducer (或 Flux 中的 Stores ) 识别它们。这个识别符我们建议你使用 string 而不是 符号(Symbols) 作为 action type , 因为 string 是可序列化的, 并且使用符号会使记录和重演变得困难。

Action Creators 生成器

建议直接清晰地创建 action type 常量。

写简单的 action creator 很容易让人厌烦, 且往往最终生成多余的样板代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}

export function editTodo(id, text) {
return {
type: 'EDIT_TODO',
id,
text
}
}

export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
}
}

你可以写一个用于生成 action creator 的函数来重载上面所有函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function makeActionCreator(type, ...argNames) {
return function(...args) {
let action = { type }
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index]
})
return action
}
}

const ADD_TODO = 'ADD_TODO'
const EDIT_TODO = 'EDIT_TODO'
const REMOVE_TODO = 'REMOVE_TODO'

export const addTodo = makeActionCreator(ADD_TODO, 'todo')
export const editTodo = makeActionCreator(EDIT_TODO, 'id', 'todo')
export const removeTodo = makeActionCreator(REMOVE_TODO, 'id')

异步 Action Creators

如果不用类似 Thunk 的 Middleware 的话就会有很多麻烦:

actionCreators.js

这样的话首先要写出 3 个 Action Creator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function loadPostsSuccess(userId, response) {
return {
type: 'LOAD_POSTS_SUCCESS',
userId,
response
};
}

export function loadPostsFailure(userId, error) {
return {
type: 'LOAD_POSTS_FAILURE',
userId,
error
};
}

export function loadPostsRequest(userId) {
return {
type: 'LOAD_POSTS_REQUEST',
userId
};
}

UserInfo.js

然后我们在每一个 Component 里面都要写下面这样的代码

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { Component } from 'react';
import { connect } from 'react-redux';
import { loadPostsRequest, loadPostsSuccess, loadPostsFailure } from './actionCreators';

class Posts extends Component {
loadData(userId) {
// 调用 React Redux `connect()` 注入的 props :
let { dispatch, posts } = this.props;

if (posts[userId]) {
// 这里是被缓存的数据!啥也不做。
return;
}

// Reducer 可以通过设置 `isFetching` 响应这个 action
// 因此让我们显示一个 Spinner 控件。
dispatch(loadPostsRequest(userId));

// Reducer 可以通过填写 `users` 响应这些 actions
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch(loadPostsSuccess(userId, response)),
error => dispatch(loadPostsFailure(userId, error))
);
}

componentDidMount() {
this.loadData(this.props.userId);
}

componentWillReceiveProps(nextProps) {
if (nextProps.userId !== this.props.userId) {
this.loadData(nextProps.userId);
}
}

render() {
if (this.props.isLoading) {
return <p>Loading...</p>;
}

let posts = this.props.posts.map(post =>
<Post post={post} key={post.id} />
);

return <div>{posts}</div>;
}
}

export default connect(state => ({
posts: state.posts
}))(Posts);

优化一下

actionCreators.js

重点在于利用 redux-thunk 来返回一个 Thunk , 使得相关的操作全部放在 Thunk 里面回调。

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 function loadPosts(userId) {
// 用 thunk 中间件解释:
return function (dispatch, getState) {
let { posts } = getState();
if (posts[userId]) {
// 这里是数据缓存!啥也不做。
return;
}

dispatch({
type: 'LOAD_POSTS_REQUEST',
userId
});

// 异步分发原味 action
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch({
type: 'LOAD_POSTS_SUCCESS',
userId,
response
}),
error => dispatch({
type: 'LOAD_POSTS_FAILURE',
userId,
error
})
);
}
}

UserInfo.js

这样的话 Component 方面的代码就会非常少了

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
30
31
import { Component } from 'react';
import { connect } from 'react-redux';
import { loadPosts } from './actionCreators';

class Posts extends Component {
componentDidMount() {
this.props.dispatch(loadPosts(this.props.userId));
}

componentWillReceiveProps(nextProps) {
if (nextProps.userId !== this.props.userId) {
this.props.dispatch(loadPosts(nextProps.userId));
}
}

render() {
if (this.props.isLoading) {
return <p>Loading...</p>;
}

let posts = this.props.posts.map(post =>
<Post post={post} key={post.id} />
);

return <div>{posts}</div>;
}
}

export default connect(state => ({
posts: state.posts
}))(Posts);

更高级的用法:自定义中间件

你可以编写你自己的中间件 你可以把上面的模式泛化, 然后代之以这样的异步 action creators :

1
2
3
4
5
6
7
8
9
10
11
12
export function loadPosts(userId) {
return {
// 要在之前和之后发送的 action types
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
// 检查缓存 (可选):
shouldCallAPI: (state) => !state.users[userId],
// 进行取:
callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
// 在 actions 的开始和结束注入的参数
payload: { userId }
};
}

解释这个 actions 的中间件可以像这样:

中间件里面对上方 Action 传过来的内容进行处理

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function callAPIMiddleware({ dispatch, getState }) {
return next => action => {
const {
types,
callAPI,
shouldCallAPI = () => true,
payload = {}
} = action

if (!types) {
// Normal action: pass it on
return next(action)
}

if (
!Array.isArray(types) ||
types.length !== 3 ||
!types.every(type => typeof type === 'string')
) {
throw new Error('Expected an array of three string types.')
}

if (typeof callAPI !== 'function') {
throw new Error('Expected callAPI to be a function.')
}

if (!shouldCallAPI(getState())) {
return
}

const [ requestType, successType, failureType ] = types

dispatch(Object.assign({}, payload, {
type: requestType
}))

return callAPI().then(
response => dispatch(Object.assign({}, payload, {
response,
type: successType
})),
error => dispatch(Object.assign({}, payload, {
error,
type: failureType
}))
)
}
}

在传给 applyMiddleware(...middlewares) 一次以后, 你能用相同方式写你的 API 调用 action creators :

最后也是最重要的, 注册这个中间件之后, 其他所有的 action creators 都可以写成这种模式

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
30
31
32
export function loadPosts(userId) {
return {
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
shouldCallAPI: (state) => !state.users[userId],
callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
payload: { userId }
};
}

export function loadComments(postId) {
return {
types: ['LOAD_COMMENTS_REQUEST', 'LOAD_COMMENTS_SUCCESS', 'LOAD_COMMENTS_FAILURE'],
shouldCallAPI: (state) => !state.posts[postId],
callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`),
payload: { postId }
};
}

export function addComment(postId, message) {
return {
types: ['ADD_COMMENT_REQUEST', 'ADD_COMMENT_SUCCESS', 'ADD_COMMENT_FAILURE'],
callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ message })
}),
payload: { postId, message }
};
}