为 Redux 准备 Counter 组件
现在 Counter 有了内部 state。我们打算把它干掉,为从 Redux 以 prop 方式获取 count
做准备。
移除顶部的 state 初始化,以及 increment
和 decrement
内部调用的 setState
。然后,把 this.state.count
替换成 this.props.count
。
Counter.js
import React from 'react'; class Counter extends React.Component { // state = { count: 0 }; // 删除 increment = () => { /* // 删除 this.setState({ count: this.state.count + 1 }); */ }; decrement = () => { /* // 同样删除 this.setState({ count: this.state.count - 1 }); */ }; render() { return ( <div className="counter"> <h2>Counter</h2> <div> <button onClick={this.decrement}>-</button> <span className="count">{ // 把 state: //// this.state.count // 替换成: this.props.count }</span> <button onClick={this.increment}>+</button> </div> </div> ); } } export default Counter;
现在 increment
和 decrement
是空的。我们会很快再次填充它们。
你会注意到 count 消失了 —— 它确实应该这样,因为目前还没有给 Counter
传递 count
prop。
连接组件和 Redux
要从 Redux 获取 count
,我们首先需要在 Counter.js 顶部引入 connect
函数。
Counter.js
import { connect } from 'react-redux';
然后我们需要在底部把 Counter 组件和 Redux 连接起来:
Counter.js
// 添加这个函数: function mapStateToProps(state) { return { count: state.count }; } // 然后把: // export default Counter; // 替换成: export default connect(mapStateToProps)(Counter);
之前我们只导出了组件本身。现在我们用 connect
函数调用把它包装起来,这样我们就可以导出已连接的 Counter。至于应用的其余部分,看起来就像一个常规组件。
然后 count 应该就重新出现了!直到我们重新实现 increment/decrement,它是不会变化的。
如何使用 React Redux connect
你可能注意到这个调用看起来有点……奇怪。为什么是 connect(mapStateToProps)(Counter)
而不是 connect(mapStateToProps, Counter)
或者 connect(Counter, mapStateToProps)
?它做了什么?
这样写是因为 connect
是一个高阶函数,它简单说就是当你调用它时会返回一个函数。然后调用返回的函数传入一个组件时,它会返回一个新(包装的)组件。
它的另一个名称是 高阶组件 (简称 "HOC")。HOCs 过去曾有过一些糟糕的新闻,但它仍然是一个相当有用的模式,connect
就是一个很好的例子。
Connect
做的是在 Redux 内部 hook,取出整个 state,然后把它传进你提供的 mapStateToProps
函数。它是个自定义函数,因为只有你知道你存在 Redux 里面的 state 的“结构”。
mapStateToProps 工作机制
connect
把整个 state 传给了你的 mapStateToProps
函数,就好像在说,“嘿,告诉我你想从这堆东西里面要什么。”
mapStateToProps
返回的对象以 props 形式传给了你的组件。以上面为例就是把 state.count
的值用 count
prop 传递:对象的属性变成了 prop 名称,它们对应的值会变成 props 的值。你看,这个函数就像字面含义一样定义从 state 到 props 的映射。
顺便说说 —— mapStateToProps
的名称是使用惯例,但并不是特定的。你可以简写成 mapState
或者用任何你想的方式调用。只要你接收 state
对象然后返回全是 props 的对象,那就没问题。
为什么不传整个 state?
在上面的例子中,我们的 state 结构已经是对的了,看起来 mapDispatchToProps
可能是不必要的。如果你实质上复制参数(state)给一个跟 state 相同的对象,这有什么意义呢?
在很小的例子中,可能会传全部 state,但通常你只会从更大的 state 集合中选择部分组件需要的数据。
并且,没有 mapStateToProps
函数,connect
不会传递任何 state。
你可以传整个 state,然后让组件梳理。但那不是一个很好的习惯,因为组件需要知道 Redux state 的结构然后从中挑选它需要的数据,后面如果你想更改结构会变得更难。
从 React 组件 Dispatch Redux Actions
现在我们的 Counter 已经被 connect
了,我们也获取到了 count
值。现在我们如何 dispatch actions 来改变 count?
好吧,connect
为你提供支持:除了传递(mapped)state,它还从 store 传递了 dispatch
函数!
要在 Counter 内部 dispatch action,我们可以调用 this.props.dispatch
携带一个 action。
我们的 reducer 已经准备好处理 INCREMENT
和 DECREMENT
actions 了,那么接下来从 increment/decrement 中 dispatch:
Counter.js
increment = () => { this.props.dispatch({ type: "INCREMENT" }); }; decrement = () => { this.props.dispatch({ type: "DECREMENT" }); };
现在我们完成了。按钮应该又重新生效了。
Action 常量
在大部分 Redux 应用中,你可以看到 action 常量都是一些简单字符串。这是一个额外的抽象级别,从长远来看可以为你节省不少时间。
Action 常量帮你避免错别字,action 命名的错别字会是一个巨大的痛苦:没有报错,没有哪里坏掉的明显标志,并且你的 action 没有做任何事情?那就可能是个错别字。
Action 常量很容易编写:用变量保存你的 action 字符串。
把这些变量放在一个 actions.js
文件里是个好办法(当你的应用很小时)。
actions.js
export const INCREMENT = "INCREMENT"; export const DECREMENT = "DECREMENT";
然后你就可以引入这些 action 名称,用它们来代替手写字符串:
Counter.js
import React from "react"; import { INCREMENT, DECREMENT } from './actions'; class Counter extends React.Component { state = { count: 0 }; increment = () => { this.props.dispatch({ type: INCREMENT }); }; decrement = () => { this.props.dispatch({ type: DECREMENT }); }; render() { ... } }
Redux Action 生成器是什么?
现在我们已经手写 action 对象。像个异教徒。
如果你有一个函数会为你编写它会怎么样?不要再误写 actinos 了!
我可以告诉你,这很疯狂。手写 { type: INCREMENT }
并保证没有弄乱有多困难?
当你的应用变得越来越大,actions 越来越多,并且这些 actions 开始变得更复杂 —— 要传更多数据而不仅是一个 type
—— action 生成器会帮上大忙。
就像 action 常量一样,但它们不是必须品。这是另一层的抽象,如果你不想在你的应用里面使用,那也没关系。
不过我还是会解释下它们是什么。然后你可以决定你是否有时/总是/绝不想使用它们。
Actions 生成器在 Redex 术语中是一个简单的函数术语,它返回一个 action 对象。就这些 :)
这是其中两个,返回熟悉的 actions。顺便说一句,它们在 action 常量的 "actions.js" 中完美契合。
actions.js
export const INCREMENT = "INCREMENT"; export const DECREMENT = "DECREMENT"; export function increment() { return { type: INCREMENT }; } export const decrement = () => ({ type: DECREMENT });
我用了两种不同方式——一个 function
和一个箭头函数——来表明你用哪种方式写并不重要。挑选你喜欢的方式就好。
你可能注意到函数命名是小写的(好吧,如果较长的话会是驼峰命名),而 action 常量会是 UPPER_CASE_WITH_UNDERSCORES
。同样,这也只是惯例。这会让你一眼区分 action 生成器和 action 常量。但你也可以按你喜欢的方式命名。Redux 并不关心。
现在,如何使用 action 生成器呢?引入然后 dispatch 就好了,当然!
Counter.js
import React from "react"; import { increment, decrement } from './actions'; class Counter extends React.Component { state = { count: 0 }; increment = () => { this.props.dispatch(increment()); // << 在这使用 }; decrement = () => { this.props.dispatch(decrement()); }; render() { ... } }
关键是要记得调用 action creator()!
不要 dispatch(increment)
🚫
应该 dispatch(increment())
✅
牢记 action 生成器是一个平凡无奇的函数。Dispatch 需要 action 是一个对象,而不是函数。
而且:你肯定会在这里出错并且非常困惑。至少一次,或许很多次。那很正常。我有时也依旧会忘记。
如何使用 React Redux mapDispatchToProps
现在你知道 action 生成器是什么,我们可以讨论又一个级别的抽象。(我知道,我知道。这是可选的。)
你知道 connect
如何传递 dispatch
函数吗?你知道你是如何厌倦一直敲 this.props.dispatch
并且它看起来多么混乱?(跟我来)
写一个 mapDispatchToProps
对象(或者函数!但通常是对象)然后传给你要包装组件的 connect
函数,你将收到这些 action 生成器作为可调用 props。看代码:
Counter.js
import React from 'react'; import { connect } from 'react-redux'; import { increment, decrement } from './actions'; class Counter extends React.Component { increment = () => { // 我们可以调用 `increment` prop, // 它会 dispatch action: this.props.increment(); } decrement = () => { this.props.decrement(); } render() { // ... } } function mapStateToProps(state) { return { count: state.count }; } // 在这个对象中, 属性名会成为 prop 的 names, // 属性值应该是 action 生成器函数. // 它们跟 `dispatch` 绑定起来. const mapDispatchToProps = { increment, decrement }; export default connect(mapStateToProps, mapDispatchToProps)(Counter);
这很棒,因为它把你从手动调用 dispatch
中解放出来。
如何使用 Redux Thunk 获取数据
既然 reducers 应该是“纯”的,我们不能做在 reducer 里面做任何 API 调用或者 dispatch actions。
我们也不能在 action 生成器里面做这些事!
但是如果我们把 action 生成器返回一个可以处理我们工作的函数会怎样呢?就像这样:
function getUser() { return function() { return fetch('/current_user'); }; }
越界了,Redux 不支持这种 actions。固执的 Redux 只接受简单对象作为 actions。
这时就需要 redux-thunk 了。它是个中间件,基本是 Redux 的一个插件,它可以使 Redux 处理像上面 getUser()
那样的 actions。
你可以像其他 action 生成器一样 dispatch 这些 "thunk actions":dispatch(getUser())
。
"thunk" 是什么?
thunk是一类函数的别名,主要特征是对另外一个函数添加了一些额外的操作,类似装饰器。其主要用途为延迟函数执行(惰性求值)或者给一个函数执行前后添加一些额外的操作。
"thunk" 是指被其它函数作为返回值的函数。
在 Redux 术语中,它是一个返回值为函数而非简单 action 对象的 action 生成器,就像这样:
function doStuff() { return function(dispatch, getState) { // 在这里 dispatch actions // 或者获取数据 // 或者该干啥干啥 } }
从技术角度讲,被返回的函数就是 "thunk",把它作为返回值的就是“action 生成器”。通常我把它们一起称为 "thunk action"。
Action 生成器返回的函数接收两个参数:dispatch
函数和 getState
。
大多数场景你只需要 dispatch
,但有时你想根据 Redux state 里面的值额外做些事情。这种情况下,调用 getState()
你就会获得整个 state 的值然后按需所取。
如何安装 Redux Thunk
使用 NPM 或者 Yarn 安装 redux-thunk,运行 npm install --save redux-thunk
。
然后,在 index.js(或者其他你创建 store 的地方),引入 redux-thunk
然后通过 Redux 的 applyMiddleware
函数把它应用到 store 中。
import thunk from 'redux-thunk'; import { createStore, applyMiddleware } from 'redux'; function reducer(state, action) { // ... } const store = createStore( reducer, applyMiddleware(thunk) );
必须确保 thunk
包装在 applyMiddleware
调用里面,否则不会生效。不要直接传 thunk
。
结合 Redux 请求数据的例子
我们从免费开放接口https://jsonplaceholder.typicode.com/posts获取全部posts数据
actions/postsActions.js
export const GET_POSTS_BEGIN = 'GET POSTS' export const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS' export const GET_POSTS_FAILURE = 'GET_POSTS_FAILURE' export const getPosts = () => ({ type: GET_POSTS_BEGIN }) export const getPostsSuccess = posts => ({ type: GET_POSTS_SUCCESS, payload: posts, }) export const getPostsFailure = () => ({ type: GET_POSTS_FAILURE }) export function fetchPosts() { return async dispatch => { dispatch(getPosts()) try { const response = await fetch('https://jsonplaceholder.typicode.com/posts') const data = await response.json() dispatch(getPostsSuccess(data)) } catch (error) { dispatch(getPostsFailure()) } } }
fetch('https://jsonplaceholder.typicode.com/posts')
是实际上请求数据的部分。然后我们在它前后分别做了一些 dispatch
调用。
Dispatch Action 来获取数据
要开始调用并且实际获取数据,我们需要 dispatch fetchPosts
action。
在哪里调用呢?
如果某一特定的组件需要数据,最好的调用地方通常是在组件刚刚加载之后,也就是它的 componentDidMount
生命周期函数。
如何给 Redux Actions 命名
获取数据的 Redux actions 通常使用标准三连:BEGIN、SUCCESS、FAILURE。这不是硬性要求,只是惯例。
BEGIN/SUCCESS/FAILURE 模式很棒,因为它给你提供钩子来跟踪发生了什么 —— 比如,设置 "loading" 标志为 "true" 以响应 BEGIN 操作,在 SUCCESS 或 FAILURE 之后设为 false
。
而且,与 Redux 中的其他所有内容一样,这个也是一个惯例,如果你不需要的话可以忽略掉。
在你调用 API 之前,dispatch BEGIN action。
调用成功之后,你可以 dispatch SUCCESS 数据。如果请求失败,你可以 dispatch 错误信息。
有时最后一个调用 ERROR。其实调用什么一点也不重要,只要你保持一致就好。
接收到 GET_POSTS_SUCCESS
action 返回的post数据后,我们写一个 reducer 把它存进 Redux store 中。开始请求时把 loading
标志设为 true,失败或者完成时设为 false。
reducers/postsReducer.js
import * as actions from '../actions/postsActions' export const initialState = { loading: false, hasErrors: false, posts: [], } export default function postsReducer(state = initialState, action) { switch (action.type) { case actions.GET_POSTS_BEGIN: return { ...state, loading: true } case actions.GET_POSTS_SUCCESS: return { posts: action.payload, loading: false, hasErrors: false } case actions.GET_POSTS_FAILURE: return { ...state, loading: false, hasErrors: true } default: return state } }
最后,我们需要把post数据传给展示它们并且也负责请求数据的 PostsPage
组件。
pages/PostsPages.js
import React, { Component } from 'react' import { connect } from 'react-redux' import { fetchPosts } from '../actions/postsActions' import { Post } from '../components/Post' class PostsPage extends Component{ componentDidMount() { this.props.fetchPosts() } render(){ const {loading, posts, hasErrors} = this.props; const renderPosts = () => { if (loading) return <p>Loading posts...</p> if (hasErrors) return <p>Unable to display posts.</p> return posts.map(post => <Post key={post.id} post={post} excerpt />) } return ( <section> <h1>Posts</h1> {renderPosts()} </section> ) } } const mapStateToProps = state => ({ loading: state.posts.loading, posts: state.posts.posts, hasErrors: state.posts.hasErrors, }) export default connect(mapStateToProps,{fetchPosts})(PostsPage)
components/Post.js
import React from 'react' export const Post = ({ post, excerpt }) => ( <article> <h2>{post.title}</h2> <p>{post.body}</p> </article> )
我指的是带有 state.posts.<whatever>
的数据而不仅仅是 state.<whatever>
,因为我假设你可能会有不止一个 reducer,每一个都处理各自的 state。为了确保这样,我们可以写一个 reducers/index.js
文件把它们放在一起:
reducers/index.js
import { combineReducers } from 'redux' import postsReducer from './postsReducer' const rootReducer = combineReducers({ posts: postsReducer, }) export default rootReducer
然后,当我们创建 store 我们可以传递这个“根” reducer:
index.js
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux' import { createStore, applyMiddleware } from 'redux'; import Counter from './Counter' import thunk from 'redux-thunk'; import PostsPage from './pages/PostsPage' import rootReducer from './reducers' const store = createStore( rootReducer, applyMiddleware(thunk) ); store.dispatch({ type: "INCREMENT" }); store.dispatch({ type: "INCREMENT" }); store.dispatch({ type: "DECREMENT" }); store.dispatch({ type: "RESET" }); const App = () => ( <Provider store={store}> <Counter/> <PostsPage /> </Provider> ); ReactDOM.render(<App />, document.getElementById('root'));
最终结果:
Redux 中错误处理
这里的错误处理比较轻量,但是对大部分调用 API 的 actions 来说基本结构是一样的。基本观点是:
- 当调用失败时,dispatch 一个 FAILURE action
- 通过设置一些标志变量和/或保存错误信息来处理 reducer 中的 FAILURE action。
- 把错误标志和信息(如果有的话)传给需要处理错误的组件,然后根据任何你觉得合适的方式渲染错误信息。
能避免重复渲染吗?
这确实个常见问题。是的,它会不止一次触发渲染。
它首先会渲染空 state,然后再渲染 loading state,接着会再次渲染展示posts。可怕!三次渲染!(如果你直接跳过 "loading" state 就可以把渲染次数将为两次)
你可能会担心不必要的渲染影响性能,但是不会:单次渲染非常快。如果你在开发的应用肉眼可见的慢的话,分析一下找出慢的原因。