React 模式(中文版)

中文版:https://reactpatterns.cn/
原版:https://reactpatterns.com

函數組件 (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 界面的時候非常有用。

如果你使用 函數組件 做爲表單元素,那就得閱讀 狀態提升 一節,把狀態轉移到上層的組件樹上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章