原文:Functional Components with React stateless functions and Ramda
浏览本文须要的学问贮备:
- 函数式编程基本概念(组合、柯里化、透镜)
- React 基本学问(组件、状况、属性、JSX)
- ES6 基本学问(class、箭头函数)
React 无状况函数
React 组件最常见的定义要领:
const List = React.createClass({
render: function() {
return (<ul>{this.props.children}</ul>);
}
});
或许运用 ES6 类语法:
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 类上设置一样。
同时也说到:
抱负情况下,大部分的组件都应该是无状况的函数,因为在将来我们可能会针对这类组件做机能优化,防止不必要的搜检和内存分派。所以引荐人人尽量的运用这类形式来开辟。
是否是以为挺风趣的?
React 社区好像越发关注经由过程 class
和 createClass
体式格局来建立组件,本日让我们来尝鲜一下无状况组件。
App 容器
起首让我们来建立一个函数式 App 容器组件,它接收一个示意运用状况的对象作为参数:
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');
简朴来讲,透镜是一种专注于给定属性的体式格局,而不关心该属性究竟是在哪一个对象上,这类体式格局便于代码复用。当我们须要把透镜运用于对象上时,能够有以下操纵:
- View
R.view(secondsElapsedLens, { secondsElapsed: 10 }); //=> 10
- Set
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 无状况组件
当我第一次读到 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 来转换无状况函数。