React 模式(中文版)
React 模式(中文版)on Github
React 模式(原版)on Github
目錄
函式元件 (Function component)
函式元件 是最簡單的一種宣告可複用元件的方法
他們就是一些簡單的函式。
function Greeting() { return <div>Hi there!</div>; }
從第一個形參中獲取屬性集 (props)
function Greeting(props) { return <div>Hi {props.name}!</div>; }
按自己的需要可以在函式元件中定義任意變數
最後一定要返回你的 React 元件。
function Greeting(props) { let style = { fontWeight: "bold", color: context.color }; return <div style={style}>Hi {props.name}!</div>; }
使用defaultProps
為任意必有屬性設定預設值
function Greeting(props) { return <div>Hi {props.name}!</div>; } Greeting.defaultProps = { name: "Guest" };
屬性解構 (Destructuring props)
解構賦值 是一種 JavaScript 特性。
出自 ES2015 版的 JavaScript 新規範。
所以看起來可能並不常見。
好比字面量賦值的反轉形式。
let person = { name: "chantastic" }; let { name } = person;
同樣適用於陣列。
let things = ["one", "two"]; let [first, second] = things;
解構賦值被用在很多函式元件 中。
下面宣告的這些元件是相同的。
function Greeting(props) { return <div>Hi {props.name}!</div>; } function Greeting({ name }) { return <div>Hi {name}!</div>; }
有一種語法可以在物件中收集剩餘屬性。
叫做剩餘引數,看起來就像這樣。
function Greeting({ name, ...restProps }) { return <div>Hi {name}!</div>; }
那三個點 (...
) 會把所有的剩餘屬性分配給restProps
物件
然而,你能使用restProps
做些什麼呢?
繼續往下看...
JSX 中的屬性展開 (JSX spread attributes)
屬性展開是JSX 中的一個的特性。
它是一種語法,專門用來把物件上的屬性轉換成 JSX 中的屬性
參考上面的屬性解構,
我們可以擴散
restProps
物件的所有屬性到 div 元素上
function Greeting({ name, ...restProps }) { return <div {...restProps}>Hi {name}!</div>; }
這讓Gretting
元件變得非常靈活。
我們可以通過傳給 Gretting 元件 DOM 屬性並確定這些屬性一定會被傳到div
上
<Greeting name="Fancy pants" className="fancy-greeting" id="user-greeting" />
避免傳遞非 DOM 屬性到元件上。 解構賦值是如此的受歡迎,是因為它可以分離元件特定的屬性
和DOM/平臺特定屬性
function Greeting({ name, ...platformProps }) { return <div {...platformProps}>Hi {name}!</div>; }
合併解構屬性和其它值 (Merge destructured props with other values)
元件就是一種抽象。
好的抽象是可以擴充套件的。
比如說下面這個元件使用class
屬性來給按鈕新增樣式。
function MyButton(props) { return <button className="btn" {...props} />; }
一般情況下這樣做就夠了,除非我們需要擴充套件其它的樣式類
<MyButton className="delete-btn">Delete...</MyButton>
在這個例子中把btn
替換成delete-btn
JSX 中的屬性展開 對先後順序是敏感的
擴散屬性中的className
會覆蓋元件上的className
。
我們可以改變它兩的順序,但是目前來說className
只有btn
。
function MyButton(props) { return <button {...props} className="btn" />; }
我們需要使用解構賦值來合併入參 props 中的className
和基礎的(元件中的)className
。 可以通過把所有的值放在一個數組裡面,然後使用一個空格連線它們。
function MyButton({ className, ...props }) { let classNames = ["btn", className].join(" "); return <button className={classNames} {...props} />; }
為了保證undefined
不被顯示在 className 上,可以使用預設值。
function MyButton({ className = "", ...props }) { let classNames = ["btn", className].join(" "); return <button className={classNames} {...props} />; }
條件渲染 (Conditional rendering)
不可以在一個元件宣告中使用 if/else 語句 You can't use if/else statements inside a component declarations.
所以可以使用條件(三元)運算子 和短路計算。
如果
{ condition && <span>Rendered when `truthy`</span>; }
除非
{ condition || <span>Rendered when `falsy`</span>; }
如果-否則
{ condition ? ( <span>Rendered when `truthy`</span> ) : ( <span>Rendered when `falsy`</span> ); }
子元素型別 (Children types)
很多型別都可以做為 React 的子元素。
多數情況下會是陣列
或者字串
。
字串String
<div>Hello World!</div>
陣列Array
<div>{["Hello ", <span>World</span>, "!"]}</div>
陣列做為子元素 (Array as children)
將陣列做為子元素是很常見的。
列表是如何在 React 中被繪製的。
我們使用map()
方法建立一個新的 React 元素陣列
<ul> {["first", "second"].map(item => ( <li>{item}</li> ))} </ul>
這和使用字面量陣列是一樣的。
<ul>{[<li>first</li>, <li>second</li>]}</ul>
這個模式可以聯合解構、JSX 屬性擴散以及其它元件一起使用,看起來簡潔無比
<ul> {arrayOfMessageObjects.map(({ id, ...message }) => ( <Message key={id} {...message} /> ))} </ul>
函式做為子元素 (Function as children)
React 元件不支援函式型別的子元素。
然而渲染屬性 是一種可以建立元件並以函式作為子元素的模式。
渲染屬性 (Render prop)
這裡有個元件,使用了一個渲染回撥函式 children。
這樣寫並沒有什麼用,但是可以做為入門的簡單例子。
const Width = ({ children }) => children(500);
元件把 children 做為函式呼叫,同時還可以傳一些引數。上面這個500
就是實參。
為了使用這個元件,我們可以在呼叫元件的時候傳入一個子元素,這個子元素就是一個函式。
<Width>{width => <div>window is {width}</div>}</Width>
我們可以得到下面的輸出。
<div>window is 500</div>
有了這個元件,我們就可以用它來做渲染策略。
<Width> {width => (width > 600 ? <div>min-width requirement met!</div> : null)} </Width>
如果有更復雜的條件判斷,我們可以使用這個元件來封裝另外一個新元件來利用原來的邏輯。
const MinWidth = ({ width: minWidth, children }) => ( <Width>{width => (width > minWidth ? children : null)}</Width> );
顯然,一個靜態的Width
元件並沒有什麼用處,但是給它繫結一些瀏覽器事件就不一樣了。下面有個實現的例子。
class WindowWidth extends React.Component { constructor() { super(); this.state = { width: 0 }; } componentDidMount() { this.setState( { width: window.innerWidth }, window.addEventListener("resize", ({ target }) => this.setState({ width: target.innerWidth }) ) ); } render() { return this.props.children(this.state.width); } }
許多開發人員都喜歡高階元件 來實現這種功能。但這只是個人喜好問題。
子元件的傳遞 (Children pass-through)
你可能會建立一個元件,這個元件會使用context
並且渲染它的子元素。
class SomeContextProvider extends React.Component { getChildContext() { return { some: "context" }; } render() { // 如果能直接返回 `children` 就完美了 } }
你將面臨一個選擇。把children
包在一個 div 中並返回,或者直接返回children
。第一種情況需要要你新增額外的標記(這可能會影響到你的樣式)。第二種將產生一個沒什麼用處的錯誤。
// option 1: extra div return <div>{children}</div>; // option 2: unhelpful errors return children;
最好把children
做為一種不透明的資料型別對待。React 提供了React.Children
方法來處理children
。
return React.Children.only(this.props.children);
代理元件 (Proxy component)
(我並不確定這個名字的準確叫法譯:代理、中介、裝飾?
)
按鈕在 web 應用中隨處可見。並且所有的按鈕都需要一個type="button"
的屬性。
<button type="button">
重複的寫這些屬性很容易出錯。我們可以寫一個高層元件來代理props
到底層元件。
const Button = props => <button type="button" {...props}>
我們可以使用Button
元件代替button
元素,並確保type
屬性始終是 button。
<Button /> // <button type="button"><button> <Button className="CTA">Send Money</Button> // <button type="button" class="CTA">Send Money</button>
樣式元件 (Style component)
這也是一種代理元件,用來處理樣式。
假如我們有一個按鈕,它使用了「primary」做為樣式類。
<button type="button" className="btn btn-primary">
我們使用一些單一功能元件來生成上面的結構。
import classnames from "classnames"; const PrimaryBtn = props => <Btn {...props} primary />; const Btn = ({ className, primary, ...props }) => ( <button type="button" className={classnames("btn", primary && "btn-primary", className)} {...props} /> );
可以視覺化的展示成下面的樣子。
PrimaryBtn() ↳ Btn({primary: true}) ↳ Button({className: "btn btn-primary"}, type: "button"}) ↳ '<button type="button" class="btn btn-primary"></button>'
使用這些元件,下面的這幾種方式會得到一致的結果。
<PrimaryBtn /> <Btn primary /> <button type="button" className="btn btn-primary" />
這對於樣式維護來說是非常好的。它將樣式的所有關注點分離到單個元件上。
組織事件 (Event switch)
當我們在寫事件處理函式的時候,通常會使用handle{事件名字}
的命名方式。
handleClick(e) { /* do something */ }
當需要新增很多事件處理函式的時候,這些函式名字會顯得很重複。這些函式的名字並沒有什麼價值,因為它們只代理了一些動作或者函式。
handleClick() { require("./actions/doStuff")(/* action stuff */) } handleMouseEnter() { this.setState({ hovered: true }) } handleMouseLeave() { this.setState({ hovered: false }) }
可以考慮寫一個事件處理函式來根據不同的event.type
來組織事件。
handleEvent({type}) { switch(type) { case "click": return require("./actions/doStuff")(/* action dates */) case "mouseenter": return this.setState({ hovered: true }) case "mouseleave": return this.setState({ hovered: false }) default: return console.warn(`No case for event type "${type}"`) } }
另外,對於簡單的元件,你可以在元件中使用箭頭函式直接呼叫匯入的動作或者函式
<div onClick={() => someImportedAction({ action: "DO_STUFF" })}
在遇到效能問題之前,不要擔心效能優化。真的不要
佈局元件 (Layout component)
佈局元件表現為一些靜態 DOM 元素的形式。它們一般並不需要經常更新。
就像下面的這個元件一樣,兩邊各自渲染了一個 children。
<HorizontalSplit leftSide={<SomeSmartComponent />} rightSide={<AnotherSmartComponent />} />
我們可以優化這個元件。
HorizontalSplit 元件是兩個子元件的父元素,我們可以告訴元件永遠都不要更新
class HorizontalSplit extends React.Component { shouldComponentUpdate() { return false; } render() { <FlexContainer> <div>{this.props.leftSide}</div> <div>{this.props.rightSide}</div> </FlexContainer> } }
容器元件 (Container component)
「容器用來獲取資料然後渲染到子元件上,僅僅如此。」—Jason Bonta
這有一個CommentList
元件。
const CommentList = ({ comments }) => ( <ul> {comments.map(comment => ( <li> {comment.body}-{comment.author} </li> ))} </ul> );
我們可以建立一個新元件來負責獲取資料渲染到上面的CommentList
函式元件中。
class CommentListContainer extends React.Component { constructor() { super() this.state = { comments: [] } } componentDidMount() { $.ajax({ url: "/my-comments.json", dataType: 'json', success: comments => this.setState({comments: comments}); }) } render() { return <CommentList comments={this.state.comments} /> } }
對於不同的應用上下文,我們可以寫不同的容器元件。
高階元件 (Higher-order component)
高階函式 是至少滿足下列一個條件的函式:
- 接受一個或多個函式作為輸入
- 輸出一個函式
所以高階元件又是什麼呢?
如果你已經用過容器元件, 這僅僅是一些泛化的元件, 包裹在一個函式中。
讓我們以Greeting
元件開始
const Greeting = ({ name }) => { if (!name) { return <div>連線中...</div>; } return <div>Hi {name}!</div>; };
如果props.name
存在,元件會渲染這個值。否則將展示「連線中...」。現在來新增點高階的感覺
const Connect = ComposedComponent => class extends React.Component { constructor() { super(); this.state = { name: "" }; } componentDidMount() { // this would fetch or connect to a store this.setState({ name: "Michael" }); } render() { return <ComposedComponent {...this.props} name={this.state.name} />; } };
這是一個返回了入參為元件的普通函式
接著,我們需要把Greeting
包裹到Connect
中
const ConnectedMyComponent = Connect(Greeting);
這是一個強大的模式,它可以用來獲取資料和給定資料到任意函式元件 中。
狀態提升 (State hoisting)
函式元件 沒有狀態 (就像名字暗示的一樣)。
事件是狀態的變化。
它們的資料需要傳遞給狀態化的父容器元件
這就是所謂的「狀態提升」。
它是通過將回調從容器元件傳遞給子元件來完成的
class NameContainer extends React.Component { render() { return <Name onChange={newName => alert(newName)} />; } } const Name = ({ onChange }) => ( <input onChange={e => onChange(e.target.value)} /> );
Name
元件從NameContainer
元件中接收onChange
回撥,並在 input 值變化的時候呼叫。
上面的alert
呼叫只是一個簡單的演示,但它並沒有改變狀態
讓我們來改變NameContainer
元件的內部狀態。
class NameContainer extends React.Component { constructor() { super(); this.state = { name: "" }; } render() { return <Name onChange={newName => this.setState({ name: newName })} />; } }
這個狀態被提升 到了容器中,通過添加回調函式,回撥中可以更新本地狀態。這就設定了一個很清晰邊界,並且使功能元件的可重用性最大化。
這個模式並不限於函式元件。因為函式元件沒有生命週期事件,你也可以在類元件中使用這種模式。
受控輸入 是一種與狀態提升同時使用時很重要的模式
(最好是在一個狀態化的元件上處理事件物件)
受控輸入 (Controlled input)
討論受控輸入的抽象並不容易。讓我們以一個不受控的(通常)輸入開始。
<input type="text" />
當你在瀏覽器中調整此輸入時,你會看到你的更改。 這個是正常的
受控的輸入不允許 DOM 變更,這使得這個模式成為可能。通過在元件範圍中設定值而不是直接在 DOM 範圍中修改
<input type="text" value="This won't change. Try it." />
顯示靜態的輸入框值對於使用者來說並沒有什麼用處。所以,我們從狀態中傳遞一個值到 input 上。
class ControlledNameInput extends React.Component { constructor() { super(); this.state = { name: "" }; } render() { return <input type="text" value={this.state.name} />; } }
然後當你改變元件的狀態的時候 input 的值就自動改變了。
return ( <input value={this.state.name} onChange={e => this.setState({ name: e.target.value })} /> );
這是一個受控的輸入框。它只會在我們的元件狀態發生變化的時候更新 DOM。這在建立一致 UI 介面的時候非常有用。
如果你使用函式元件 做為表單元素,那就得閱讀狀態提升 一節,把狀態轉移到上層的元件樹上。