搭配 React
这里需要再强调一下:R 和 React 之间没有关系。R React、Angular、Ember、jQuery 甚至纯 JavaScript。
尽管如此,R 还是和 和 这类库搭配起来用最好,因为这类库允许你以 state 的形式来描述界面,R 通过 action 的形式来发起 state 变化。
下面使用 React 来开发 todo 任务管理应用。
安装 React R
R 认并不包含 ,需要单独安装。
npm install --save react-r
如果你不使用 npm,你也可以从 unpkg 最新的 UMD 包(和)。如果你用 <script> 的方式引入 UMD 包,那么它会在全局抛出window.ReactR对象。
R 的 React 绑定库是基于 的开发思想。所以建议先读完这篇再回来继续学习。这个思想非常重要。
已经读完了?那让我们再总结一下不同点:
大部分的组件都应该是展示型的,但一般需要少数的几个容器组件把它们和 R store 连接起来。这和下面的设计简介并不意味着容器组件必须位于组件树的最顶层。如果容器组件变得太复杂(例如,它有大量的嵌套组件以及传递数不尽的回调),那么在组件树中引入另容器,就像FAQ中提到的那样
技术上讲你可以直接使用 store.subscribe() 来编写容器组件。但不建议这么做的原因是无法使用 React R 带来的优化。也因此,不要手写容器组件,而使用 React R 的 connect() 来,后面会详细介绍。
设计组件层次结构
还记得当初如何设计state根对象的结构 吗?现在就要定义与它匹配的界面的层次结构。其实这不是 R 相关的工作,方面解释的非常棒。
我们的概要设计很简单。我们想要 todo 项的列表。 todo 项被点击后,会一条线并 completed。我们会新增 todo 字段。在 footer 里可切换的全部/只 completed 的/只 incompleted 的 todos。
以下的这些组件(和它们的 props )就是从这个设计里来的:
TodoList 用于 todos 列表。
todos: Array 以 { text, completed } 形式的 todo 项数组。
onTodoClick(index: number) 当 todo 项被点击时的回调。
Todo todo 项。
text: string 的文本。
completed: boolean todo 项是否线。
onClick() 当 todo 项被点击时的回调。
Link 带有 callback 回调的
onClick() 当点击时会触发
Footer 允许改变可见 todo 过滤器的组件。
App 根组件,渲染余下的所有。
这些组件只定义外观并不关心数据来源和如何改变。传入什么就渲染什么。如果你把从 R 迁移到别的架构,这些组件可以不做任何改动直接使用。它们并不依赖于 R。
还需要一些容器组件来把展示组件连接到 R。例如,展示型的 TodoList 组件需要类似 VisibleTodoList 的容器来监听 R store 变化并处理如何过滤出要的数据。为了实现状态过滤,需要实现 FilterLink 的容器组件来渲染 Link 并在点击时触发对应的 action:
VisibleTodoList 根据当前的状态来对 todo 列表进行过滤,并渲染 TodoList。
FilterLink 得到当前过滤器并渲染 Link。
filter: string 就是当前过滤的状态
有时很难分清到底该使用容器组件还是展示组件。例如,有时表单和严重耦合在一起,如这个小的组件:
AddTodo 含有“Add”按钮的输入框
技术上讲可以把它分成两个组件,但一开始就这么做有点早。在一些非常小的组件里混用容器和展示是可以的。当业务变复杂后,如何拆分就很明显了。所以现在就使用混合型的吧。
终于开始开发组件了!先做展示组件,这样可以先不考虑 R。
它们只是普通的 React 组件,所以不会详细解释。我们会使用式无状态组件除非需要本地 state 或生命周期的场景。这并不是说展示组件必须是 -- 只是因为这样做容易些。如果你需要使用本地 state,生命周期,或者优化,可以将它们转成 class。
import React from 'react' import PropTypes from 'prop-types' const Todo = ({ onClick, completed, text }) => ( <li onClick={onClick} style={{ textDecoration: completed ? 'line-through' : 'none' }} > {text} </li> ) Todo.propTypes = { onClick: PropTypes.func.is, completed: PropTypes.bool.is, text: PropTypes.string.is } export default Todo
import React from 'react' import PropTypes from 'prop-types' import Todo from './Todo' const TodoList = ({ todos, onTodoClick }) => ( <ul> {todos.map((todo, index) => ( <Todo key={index} {...todo} onClick={() => onTodoClick(index)} /> ))} </ul> ) TodoList.propTypes = { todos: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.number.is, completed: PropTypes.bool.is, text: PropTypes.string.is }).is ).is, onTodoClick: PropTypes.func.is } export default TodoList
import React from 'react' import PropTypes from 'prop-types' const Link = ({ active, children, onClick }) => { if (active) { return <span>{children}</span> } return ( <a href="" onClick={e => { e.preventDefault() onClick() }} > {children} </a> ) } Link.propTypes = { active: PropTypes.bool.is, children: PropTypes.node.is, onClick: PropTypes.func.is } export default Link
import React from 'react' import FilterLink from '../containers/FilterLink' const Footer = () => ( <p> Show: <FilterLink filter="SHOW_ALL">All</FilterLink> {', '} <FilterLink filter="SHOW_ACTIVE">Active</FilterLink> {', '} <FilterLink filter="SHOW_COMPLETED">Completed</FilterLink> </p> ) export default Footer
现在来创建一些容器组件把这些展示组件和 R 关联起来。技术上讲,容器组件就是使用 store.subscribe() 从 R state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。你可以手工来开发容器组件,但建议使用 React R 库的 来,这个做了优化来避免很多不必要的重复渲染。(这样你就不必为了而手动实现 中的 shouldComponentUpdate 。)
使用 connect() 前,需要先定义 mapStateToProps 这个来指定如何把当前 R store state 映射到展示组件的 props 中。例如,VisibleTodoList 需要计算传到 TodoList 中的 todos,所以定义了根据 state.visibilityFilter 来过滤 state.todos 的,并在 mapStateToProps 中使用。
const getVisibleTodos = (todos, filter) => { switch (filter) { case 'SHOW_COMPLETED': return todos.filter(t => t.completed) case 'SHOW_ACTIVE': return todos.filter(t => !t.completed) case 'SHOW_ALL': default: return todos } } const mapStateToProps = state => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } }
除了读取 state,容器组件还能分发 action。类似的方式,可以定义 mapDispatchToProps() 接收 dispatch() 并返回期望注入到展示组件的 props 中的回调。例如,我们希望 VisibleTodoList 向 TodoList 组件中注入叫 onTodoClick 的 props ,还希望 onTodoClick 能分发 TOGGLE_TODO 这个 action:
const mapDispatchToProps = dispatch => { return { onTodoClick: id => { dispatch(toggleTodo(id)) } } }
最后,使用 connect() 创建 VisibleTodoList,并传入这两个。
import { connect } from 'react-r' const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList) export default VisibleTodoList
这就是 React R API 的基础,但还漏了一些快捷技巧和强大的配置。建议你仔细学习 。如果你担心 mapStateToProps 创建新对象太过频繁,可以学习如何使用 来计算衍生数据。
其它容器组件定义如下:
import { connect } from 'react-r' import { setVisibilityFilter } from '../actions' import Link from '../components/Link' const mapStateToProps = (state, ownProps) => { return { active: ownProps.filter === state.visibilityFilter } } const mapDispatchToProps = (dispatch, ownProps) => { return { onClick: () => { dispatch(setVisibilityFilter(ownProps.filter)) } } } const FilterLink = connect( mapStateToProps, mapDispatchToProps )(Link) export default FilterLink
import { connect } from 'react-r' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList' const getVisibleTodos = (todos, filter) => { switch (filter) { case 'SHOW_ALL': return todos case 'SHOW_COMPLETED': return todos.filter(t => t.completed) case 'SHOW_ACTIVE': return todos.filter(t => !t.completed) } } const mapStateToProps = state => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } } const mapDispatchToProps = dispatch => { return { onTodoClick: id => { dispatch(toggleTodo(id)) } } } const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList) export default VisibleTodoList
回想一下前面提到的, AddTodo 组件的视图和逻辑混合在单独的定义之中。
import React from 'react' import { connect } from 'react-r' import { addTodo } from '../actions' let AddTodo = ({ dispatch }) => { let input return ( <div> <form onSubmit={e => { e.preventDefault() if (!input.value.trim()) { return } dispatch(addTodo(input.value)) input.value = '' }} > <input ref={node => { input = node }} /> <button type="submit">Add Todo</button> </form> </div> ) } AddTodo = connect()(AddTodo) export default AddTodo
如果你不熟悉 ref , 请阅读这篇以熟悉这个的推荐。
import React from 'react' import Footer from './Footer' import AddTodo from '../containers/AddTodo' import VisibleTodoList from '../containers/VisibleTodoList' const App = () => ( <div> <AddTodo /> <VisibleTodoList /> <Footer /> </div> ) export default App
传入 Store
所有容器组件都可以访问 R store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store 把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了容器组件。
建议的方式是使用指定的 React R 组件 来 让所有容器组件都可以访问 store,而不必显式地传递它。只需要在渲染根组件时使用即可。
import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-r' import { createStore } from 'r' import todoApp from './reducers' import App from './components/App' let store = createStore(todoApp) render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
接下来
参照本完整示例来深化理解。然后就可以跳到高级教程学习网络请求处理和路由。