原文: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 來轉換無狀態函數。