React 事件處理

 

事件處理

使用 React element 處理事件跟使用 DOM element 處理事件是十分相似的。它們有一些語法上的差異:

  • 事件的名稱在 React 中都是 camelCase,而在 HTML DOM 中則是小寫。
  • 事件的值在 JSX 中是一個 function,而在 HTML DOM 中則是一個 string。

例如,在 HTML 中的語法:

<button onclick="activateLasers()">
  Activate Lasers
</button>

和在 React 中的語法有些微的不同:

<button onClick={activateLasers}>  Activate Lasers
</button>

另外一個差異是,在 React 中,你不能夠使用 return false 來避免瀏覽器預設行為。你必須明確地呼叫 preventDefault。例如,在純 HTML 中,若要避免連結開啟新頁的預設功能,你可以這樣寫:

<form onsubmit="console.log('You clicked submit.'); return false">
  <button type="submit">Submit</button>
</form>

在 React 中,你則可以這樣寫:

function Form() {
  function handleSubmit(e) {
    e.preventDefault();    console.log('You clicked submit.');
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Submit</button>
    </form>
  );
}

在這裡,e 是一個 synthetic 事件(synthetic event)。React 根據 W3C 規範來定義這些 synthetic 事件,React event 與 native event 工作的方式不盡然相同。若想了解更多這方面的資訊,請參考 SyntheticEvent

當使用 React 時,你不需要在建立一個 DOM element 後再使用 addEventListener 來加上 listener。你只需要在這個 element 剛開始被 render 時就提供一個 listener。

當你使用 ES6 class 來定義 Component 時,常見的慣例是把 event handler 當成那個 class 的方法。例如,這個 Toggle Component 會 render 一個按鈕,讓使用者可以轉換 state 中的「開」與「關」:

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // 為了讓 `this` 能在 callback 中被使用,這裡的綁定是必要的:    this.handleClick = this.handleClick.bind(this);  }

  handleClick() {    this.setState(prevState => ({      isToggleOn: !prevState.isToggleOn    }));  }
  render() {
    return (
      <button onClick={this.handleClick}>        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

ReactDOM.render(
  <Toggle />,
  document.getElementById('root')
);

請特別注意 this 在 JSX callback 中的意義。在 JavaScript 中,class 的方法在預設上是沒有被綁定(bound)的。如果你忘了綁定 this.handleClick 並把它傳遞給 onClick 的話,this 的值將會在該 function 被呼叫時變成 undefined

這並非是 React 才有的行為,而是 function 在 JavaScript 中的運作模式。總之,當你使用一個方法,卻沒有在後面加上 () 之時(例如當你使用 onClick={this.handleClick} 時),你應該要綁定這個方法。

如果呼叫 bind 對你來說很麻煩的話,你可以用別的方式。如果你使用了還在測試中的 class fields 語法的話,你可以用 class field 正確的綁定 callback:

class LoggingButton extends React.Component {
  // 這個語法確保 `this` 是在 handleClick 中被綁定:  // 警告:這是一個還在*測試中*的語法:  handleClick = () => {    console.log('this is:', this);  }
  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

這個語法在 Create React App 中是預設成可行的。

如果你並沒有使用 class field 的語法的話,你則可以在 callback 中使用 arrow function

class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    // 這個語法確保 `this` 是在 handleClick 中被綁定:    return (      <button onClick={() => this.handleClick()}>        Click me
      </button>
    );
  }
}

這個語法的問題是每一次 LoggingButton render 的時候,就會建立一個不同的 callback。大多時候,這是無所謂的。然而,如果這個 callback 被當作一個 prop 傳給下層的 component 的話,其他的 component 也許會做些多餘的 re-render。原則上來說,我們建議在 constructor 內綁定,或使用 class field 語法,以避免這類的性能問題。

將參數傳給 Event Handler

在一個迴圈中,我們常常會需要傳遞一個額外的參數給 event handler。例如,如果 id 是每一行的 ID 的話,下面兩種語法都可行:

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

以上這兩行程式是相同的。一個使用 arrow functions,另一個則使用了Function.prototype.bind

以這兩個例子來說,e 這個參數所代表的 React 事件將會被當作 ID 之後的第二個參數被傳遞下去。在使用 arrow function 時,我們必須明確地將它傳遞下去,但若使用 bind 語法,未來任何的參數都將會自動被傳遞下去。

React State

 

State 和生命週期

這個章節會介紹在 React component 中 state 以及生命週期的概念。


在這個章節中,我們將會學習如何封裝 Clock component 讓它可以真正的被重複使用。它將會設定本身的 timer 並且每秒更新一次。

我們可以像這樣封裝 clock 做為開始:

function Clock(props) {
  return (
    <div>      <h1>Hello, world!</h1>      <h2>It is {props.date.toLocaleTimeString()}.</h2>    </div>  );
}

function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,    document.getElementById('root')
  );
}

setInterval(tick, 1000);

然而,它缺少了一個重要的需求:Clock 設定 timer 並在每秒更新 UI 應該是 Clock 實作的細節的事實。

理想情況下,我們想要撰寫一次 Clock 並且它會自己更新:

ReactDOM.render(
  <Clock />,  document.getElementById('root')
);

如果要實現這個理想情況,我們需要加入「state」到 Clock component。

State 類似於 prop,但它是私有且由 component 完全控制的。

component 被定義為 class 有一些額外的特性。Local state 就是 class 其中的一個特性。

改為以下方式:建立一個Clock.js

import React from "react";

export default class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }
  render() {
    return (
      <div>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}



之後在index.js下引入這個class
ReactDOM.render(
  <React.StrictMode>
    {/* <App /> */}
    {/* <SaySomething text="Hihihi" /> */}
    <Clock />
  </React.StrictMode>,
  document.getElementById("root")
);



加入生命週期方法到 Class

在具有許多 component 的應用程式中,當 component 被 destroy 時,釋放所佔用的資源是非常重要的。

每當 Clock render 到 DOM 的時候,我們想要設定一個 timer。在 React 中稱為「mount」。

每當產生的 Clock DOM 被移除時,我們想要清除 timer。在 React 中稱為「unmount」。

每當 component 在 mount 或是 unmount 的時候,我們可以在 component class 上宣告一些特別的方法來執行一些程式碼:

這些方法被稱為「生命週期方法」。

componentDidMount() 方法會在 component 被 render 到 DOM 之後才會執行。這是設定 timer 的好地方:

componentDidMount() {
    this.timerID = setInterval(() => this.tick(), 1000);
  }

注意我們是如何正確的在 thisthis.timerID) 儲存 timer ID。

雖然 this.props 是由 React 本身設定的,而且 this.state 具有特殊的意義,如果你需要儲存一些不相關於資料流的內容(像是 timer ID),你可以自由的手動加入。




我們將會在 componentWillUnmount() 生命週期方法內移除 timer:

  componentWillUnmount() {
    clearInterval(this.timerID);
  }


最後,我們將會實作一個 tick() 的方法,Clock component 將會在每秒執行它。

它將會使用 this.setState() 來安排 component local state 的更新:

import React from "react";

export default class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }

  componentDidMount() {
    this.timerID = setInterval(() => this.tick(), 1000);
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date(),
    });
  }

  render() {
    return (
      <div>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

讓我們快速的回顧一下發生了哪些事情,以及呼叫這些方法的順序:

  1. 當 <Clock /> 被傳入到 ReactDOM.render() 時,React 會呼叫 Clock component 的constructor。由於 Clock 需要顯示目前的時間,它使用包含目前時間的 object 初始化 this.state。我們會在之後更新這個 state。
  2. React 接著呼叫 Clock component 的 render() 方法。這就是 React 如何了解應該要在螢幕上顯示什麼內容。React 接著更新 DOM 來符合 Clock 的 render 輸出。
  3. 每當 Clock 輸出被插入到 DOM 時,React 會呼叫 componentDidMount() 生命週期方法。在 Clock component 生命週期方法內,會要求瀏覽器設定 timer 每秒去呼叫 component 的 tick() 方法。
  4. 瀏覽器每秒呼叫 tick() 方法。其中,Clock component 透過包含目前時間的 object 呼叫 setState() 來調度 UI 更新。感謝 setState(),React 現在知道 state 有所改變,並且再一次呼叫 render() 方法來了解哪些內容該呈現在螢幕上。這時候,在 render() 方法內的 this.state.date 將會有所不同,因此 render 輸出將會是更新的時間。React 相應地更新 DOM。
  5. 如果 Clock component 從 DOM 被移除了,React 會呼叫 componentWillUnmount() 生命週期方法,所以 timer 會被停止。



正確的使用 State

有三件關於 setState() 的事情你應該要知道。

請不要直接修改 State

例如,這將不會重新 render component:

// 錯誤
this.state.comment = 'Hello';

相反的,使用 setState()

// 正確
this.setState({comment: 'Hello'});

你唯一可以指定 this.state 值的地方是在 constructor。

State 的更新可能是非同步的

React 可以將多個 setState() 呼叫批次處理為單一的更新,以提高效能。

因為 this.props 和 this.state 可能是非同步的被更新,你不應該依賴它們的值來計算新的 state。

例如,這個程式碼可能無法更新 counter:

// 錯誤
this.setState({
  counter: this.state.counter + this.props.increment,
});

要修正這個問題,使用第二種形式的 setState(),它接受一個 function 而不是一個 object。Function 將接收先前的 state 作為第一個參數,並且將更新的 props 作為第二個參數:

// 正確
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

在上面我們使用 arrow function,但它也可以適用於正常的 function:

// 正確
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

State 的更新將會被 Merge

當你呼叫 setState() 時,React 會 merge 你提供的 object 到目前的 state。

例如,你的 state 可能包含幾個單獨的變數:

  constructor(props) {
    super(props);
    this.state = {
      posts: [],      comments: []    };
  }

然後你可以單獨的呼叫 setState() 更新它們:

  componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts      });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments      });
    });
  }

這個 merge 是 shallow 的,所以 this.setState({comments}) 保持 this.state.posts 的完整,但它完全取代了 this.state.comments

向下資料流

Parent 和 child component 不會知道某個 component 是 stateful 或 stateless 的 component,而且它們不在意它是透過 function 或是 class 被定義的。

這就是 state 通常被稱為 local state 或被封裝的原因。除了擁有和可以設定它之外的任何 component 都不能訪問它。

Component 可以選擇將它的 state 做為 props 往下傳遞到它的 child component:

<FormattedDate date={this.state.date} />

FormattedDate component 會在它的 props 接收到 date,但他不知道它是從 Clock 的 state 傳遞過來的,從 Clock 的 props 或者是透過手動輸入:

function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

這通常被稱作為「上至下」或「單向」的資料流。任何 state 總是由某個特定的 component 所擁有,任何從 state 得到的資料或 UI,state 只能影響在 tree「以下」的 component。

如果你想像一個 component tree 是一個 props 的瀑布,每個 component 的 state 像是一個額外的水流源頭,它在任意的某個地方而且往下流。

為了表示所有 component 真的都是被獨立的,我們可以建立一個 App component 來 render 三個 <Clock>

function App() {
  return (
    <div>
      <Clock />      <Clock />      <Clock />    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

每個 Clock 設定它本身的 timer 並獨立的更新。

在 React 應用程式中,不論 component 是 stateful 或 stateless 都被視為是實作 component 的細節,它可能隨著時間而改變。你可以在 stateful component 內使用 stateless component,反之亦然。