原文:Functional Components with React stateless functions and Ramdanode
阅读本文须要的知识储备:react
React 组件最多见的定义方法:git
const List = React.createClass({ render: function() { return (<ul>{this.props.children}</ul>); } });
或者使用 ES6 类语法:github
class List extends React.Component { render() { return (<ul>{this.props.children}</ul>); } }
又或者使用普通的 JS 函数:编程
// 无状态函数语法 const List = function(children) { return (<ul>{children}</ul>); }; //ES6 箭头函数语法 const List = (children) => (<ul>{children}</ul>);
React 官方文档对这种组件作了如下说明:数组
这种简化的组件 API 适用于仅依赖属性的纯函数组件。这些组件不容许拥有内部状态,不会生成组件实例,也没有组件的生命周期方法。它们只对输入进行纯函数转换。不过开发者仍然能够为它们指定.propTypes
和.defaultProps
,只须要设置为函数的属性就能够了,就跟在 ES6 类上设置同样。
同时也说到:性能优化
理想状况下,大部分的组件都应该是无状态的函数,由于在将来咱们可能会针对这类组件作性能优化,避免没必要要的检查和内存分配。因此推荐你们尽量的使用这种模式来开发。
是否是以为挺有趣的?app
React 社区彷佛更加关注经过 class
和 createClass
方式来建立组件,今天让咱们来尝鲜一下无状态组件。less
首先让咱们来建立一个函数式 App 容器组件,它接受一个表示应用状态的对象做为参数:dom
import React from 'react'; import ReactDOM from 'react-dom'; const App = appState => (<div className="container"> <h1>App name</h1> <p>Some children here...</p> </div>);
而后,定义一个 render
方法,做为 App
函数的属性:
import React from 'react'; import ReactDOM from 'react-dom'; import R from 'ramda'; const App = appState => (<div className="container"> <h1>App name</h1> <p>Some children here...</p> </div>); App.render = R.curry((node, props) => ReactDOM.render(<App {...props}/>, node)); export default App;
等等!有点看不明白了!
为何咱们须要一个柯里化的渲染函数?又为何渲染函数的参数顺序反过来了?
别急别急,这里惟一要说明的是,因为咱们使用的是无状态组件,因此状态必须由其它地方来维护。也就是说,状态必须由外部维护,而后经过属性的方式传递给组件。
让咱们来看一个具体的计时器例子。
一个简单的计时器组件只接受一个属性 secondsElapsed
:
import React from 'react'; export default ({ secondsElapsed }) => (<div className="well"> Seconds Elapsed: {secondsElapsed} </div>);
把它添加到 App
中:
import React from 'react'; import ReactDOM from 'react-dom'; import R from 'ramda'; import Timer from './timer'; const App = appState => (<div className="container"> <h1>App name</h1> <Timer secondsElapsed={appState.secondsElapsed} /> </div>); App.render = R.curry((node, props) => ReactDOM.render(<App {...props}/>, node)); export default App;
最后,建立 main.js
来渲染 App
:
import App from './components/app'; const render = App.render(document.getElementById('app')); let appState = { secondsElapsed: 0 }; //first render render(appState); setInterval(() => { appState.secondsElapsed++; render(appState); }, 1000);
在进一步说明以前,我想说,appState.secondElapsed++
这种修改状态的方式让我以为很是不爽,不过稍后咱们会使用更好的方式来实现。
这里咱们能够看出,render
其实就是用新属性来从新渲染组件的语法糖。下面这行代码:
const render = App.render(document.getElementById(‘app’));
会返回一个具备 (props) => ReactDOM.render(...)
函数签名的函数。
这里并无什么太难理解的内容。每当 secondsElapsed
的值改变后,咱们只须要从新调用 render
方法便可:
setInterval(() => { appState.secondsElapsed++; render(appState); }, 1000);
如今,让咱们来实现一个相似 Redux 风格的归约函数,以不断的递增 secondsElapsed
。归约函数是不容许修改当前状态的,全部最简单的实现方式就是 currentState -> newState
。
这里咱们使用 Ramda 的透镜(Lens)来实现 incSecondsElapsed
函数:
const secondsElapsedLens = R.lensProp('secondsElapsed'); const incSecondsElapsed = R.over(secondsElapsedLens, R.inc); setInterval(() => { appState = incSecondsElapsed(appState); render(appState); }, 1000);
第一行代码中,咱们建立了一个透镜:
const secondsElapsedLens = R.lensProp('secondsElapsed');
简单来讲,透镜是一种专一于给定属性的方式,而不关心该属性究竟是在哪一个对象上,这种方式便于代码复用。当咱们须要把透镜应用于对象上时,能够有如下操做:
R.view(secondsElapsedLens, { secondsElapsed: 10 }); //=> 10
R.set(secondsElapsedLens, 11, { secondsElapsed: 10 }); //=> 11
R.over(secondsElapsedLens, R.inc, { secondsElapsed: 10 }); //=> 11
咱们实现的 incSecondsElapsed
就是对 R.over
进行局部应用的结果。
const incSecondsElapsed = R.over(secondsElapsedLens, R.inc);
该行代码会返回一个新函数,一旦调用时传入 appState
,就会把 R.inc
应用在 secondsElapsed
属性上。
须要注意的是,Ramda 历来都不会修改对象,因此咱们须要本身来处理脏活:
appState = incSecondsElapsed(appState);
若是想支持 undo/redo ,只须要维护一个历史数组记录下每一次状态便可,或者使用 Redux 。
目前为止,咱们已经品尝了柯里化和透镜,下面让咱们继续品尝组合。
当我第一次读到 React 无状态组件时,我就在想可否使用 R.compose
来组合这些函数呢?答案很明显,固然是 YES 啦:)
让咱们从一个 TodoList 组件开始:
const TodoList = React.createClass({ render: function() { const createItem = function(item) { return (<li key={item.id}>{item.text}</li>); }; return (<div className="panel panel-default"> <div className="panel-body"> <ul> {this.props.items.map(createItem)} </ul> </div> </div>); } });
如今问题来了,TodoList 可否经过组合更小的、可复用的组件来实现呢?固然,咱们能够把它分割成 3 个小组件:
const Container = children => (<div className="panel panel-default"> <div className="panel-body"> {children} </div> </div>);
const List = children => (<ul> {children} </ul>);
const ListItem = ({ id, text }) => (<li key={id}> <span>{text}</span> </li>);
如今,咱们来一步一步看,请必定要在理解了每一步以后才往下看:
Container(<h1>Hello World!</h1>); /** * <div className="panel panel-default"> * <div className="panel-body"> * <h1>Hello World!</h1> * </div> * </div> */ Container(List(<li>Hello World!</li>)); /** * <div className="panel panel-default"> * <div className="panel-body"> * <ul> * <li>Hello World!</li> * </ul> * </div> * </div> */ const TodoItem = { id: 123, text: 'Buy milk' }; Container(List(ListItem(TodoItem))); /** * <div className="panel panel-default"> * <div className="panel-body"> * <ul> * <li> * <span>Buy milk</span> * </li> * </ul> * </div> * </div> */
没有什么太特别的,只不过是一步一步的传参调用。
接着,让咱们来作一些组合的练习:
R.compose(Container, List)(<li>Hello World!</li>); /** * <div className="panel panel-default"> * <div className="panel-body"> * <ul> * <li>Hello World!</li> * </ul> * </div> * </div> */ const ContainerWithList = R.compose(Container, List); R.compose(ContainerWithList, ListItem)({id: 123, text: 'Buy milk'}); /** * <div className="panel panel-default"> * <div className="panel-body"> * <ul> * <li> * <span>Buy milk</span> * </li> * </ul> * </div> * </div> */ const TodoItem = { id: 123, text: 'Buy milk' }; const TodoList = R.compose(Container, List, ListItem); TodoList(TodoItem); /** * <div className="panel panel-default"> * <div className="panel-body"> * <ul> * <li> * <span>Buy milk</span> * </li> * </ul> * </div> * </div> */
发现了没!TodoList
组件已经被表示成了 Container
、List
和 ListItem
的组合了:
const TodoList = R.compose(Container, List, ListItem);
等等!TodoList
这个组件只接受一个 todo 对象,可是咱们须要的是映射整个 todos 数组:
const mapTodos = function(todos) { return todos.map(function(todo) { return ListItem(todo); }); }; const TodoList = R.compose(Container, List, mapTodos); const mock = [ {id: 1, text: 'One'}, {id: 1, text: 'Two'}, {id: 1, text: 'Three'} ]; TodoList(mock); /** * <div className="panel panel-default"> * <div className="panel-body"> * <ul> * <li> * <span>One</span> * </li> * <li> * <span>Two</span> * </li> * <li> * <span>Three</span> * </li> * </ul> * </div> * </div> */
可否以更函数式的方式简化 mapTodos
函数?
// 下面的代码 return todos.map(function(todo) { return ListItem(todo); }); // 等效于 return todos.map(ListItem); // 因此变成了 const mapTodos = function(todos) { return todos.map(ListItem); }; // 等效于使用 Ramda 的方式 const mapTodos = function(todos) { return R.map(ListItem, todos); }; // 注意 Ramda 的两个特色: // - Ramda 函数默认都支持柯里化 // - 为了便于柯里化,Ramda 函数的参数进行了特定排列, // 待处理的数据一般放在最后 // 所以: const mapTodos = R.map(ListItem); //此时就再也不须要 mapTodos 了: const TodoList = R.compose(Container, List, R.map(ListItem));
哒哒哒!完整的 TodoList
实现代码以下:
import React from 'React'; import R from 'ramda'; const Container = children => (<div className="panel panel-default"> <div className="panel-body"> {children} </div> </div>); const List = children => (<ul> {children} </ul>); const ListItem = ({ id, text }) => (<li key={id}> <span>{text}</span> </li>); const TodoList = R.compose(Container, List, R.map(ListItem)); export default TodoList;
其实,还少了同样东西,不过立刻就会加上。在那以前让咱们先来作些准备:
let appState = { secondsElapsed: 0, todos: [ {id: 1, text: 'Buy milk'}, {id: 2, text: 'Go running'}, {id: 3, text: 'Rest'} ] };
TodoList
到 App
import TodoList from './todo-list'; const App = appState => (<div className="container"> <h1>App name</h1> <Timer secondsElapsed={appState.secondsElapsed} /> <TodoList todos={appState.todos} /> </div>);
TodoList
接受的是一个 todos 数组,可是这里倒是:
<TodoList todos={appState.todos} />
咱们把列表传递做为一个属性,因此等效于:
TodoList({todos: appState.todos});
所以,咱们必须修改 TodoList
,以便让它接受一个对象而且取出 todos
属性:
const TodoList = R.compose(Container, List, R.map(ListItem), R.prop('todos'));
这里并无什么高深技术。仅仅是从右到左的组合,R.prop('todos')
会返回一个函数,调用该函数会返回其做为的参数对象的 todos
属性,接着把该属性值传递给 R.map(ListItem)
,如此往复:)
以上就是本文的尝鲜内容。但愿能对你们有所帮助,这仅仅是我基于 React 和 Ramda 作的一部分实验。将来,我会努力尝试覆盖高阶组件和使用 Transducer 来转换无状态函数。