跳至主要内容

[react] 元件(component)

未閱讀:Writing Resilient Components @ OverReacted

建立元件(Component)

React 中的元件(component)是一個小而可重複使用的程式碼,每一個元件都必須從 Component 這個類別(class)而來component class 就像是一個可以用來建立許多不同元件的工廠。

render() 方法中,記得要使用 return 回傳樣版

import React from 'react';
import ReactDOM from 'react-dom';

class MyComponentClass extends React.Component {
render() {
return <h1>Hello world</h1>;
}
}

const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<MyComponentClass />);

轉譯多行的樣版(template)

當在 React Component 中要轉譯多行的樣版時,要使用括號 () 把多行的程式碼包起來

import React from 'react';
import ReactDOM from 'react-dom';

class QuoteMaker extends React.Component {
render() {
return (
<blockquote>
<p>What is important now is to recover our senses.</p>
<cite>
<a target="_blank" href="https://en.wikipedia.org/wiki/Susan_Sontag">
Susan Sontag
</a>
</cite>
</blockquote>
);
}
}

const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<QuoteMaker />);

Stateful 或 Stateless

在 React 中,Stateful 指的是帶有 state 屬性的元件;而 stateless 則是指任何不帶有 state 屬性的元件。

在 React 元件中,props 只可以透過自己以外的其他元件來修改它,絕不應該更新它自己的 this.props;而 state 則相反,只應該透過自己更新 this.state,其他的元件都不應該更改自己以外其他的元件。

States

在 React 元件中有兩種方式可以存取動態的資料,分別是 propsstate。與 props 不同的地方在於,state 不是透過外部傳進來的,而是由一個元件決定它自己內部 state

定義 States

我們可以在 Class(類別)的建構式(constructor)中,透過 this.state 來定義 state 的內容:

class Example extends React.Component {
constructor(props) {
super(props);
this.state = { mood: 'decent' };
}

render() {
return <h1>I'm feeling {this.state.mood}!</h1>;
}
}

<Example />;

使用 States

keywords: this.state

在元件中,只需要透過 this.state.name-of-property 就可以取得 state 的內容。

更新 State

keywords: this.setState

使用 this.setState 可以更新資料狀態(state),這個方法會將新的物件合併(merge)進原本的物件,因此資料狀態中沒有被更新到的部分,則會保留原有的狀態。

每當呼叫 this.setState() 時,this.setState() 會在資料狀態改變時,自動呼叫 .render() ,進而導致畫面重新轉譯。

簡單來說,this.setState() 之後,會自動呼叫 .render(),因此不能在 render 函式中使用 this.setState(),因為這將導致無窮迴圈。

需要留意的是,在 <button onClick={this.toggleMood}> 中,它會尚失其原本的 this 所指稱的對象,因此需要在 constructor 中先使用 this.toggleMood = this.toggleMood.bind(this); 把指稱到的 this 綁定好:

import React from 'react';
import ReactDOM from 'react-dom';

class Mood extends React.Component {
constructor(props) {
super(props);
this.state = { mood: 'good' };

// 這段是必要的,如果沒有這樣寫的話, toggleMood 在 onClick 的 callback 執行時,
// this 會變成 Window 物件,而不是原本的 Mood 這個類別。
this.toggleMood = this.toggleMood.bind(this);
}

toggleMood() {
const newMood = this.state.mood == 'good' ? 'bad' : 'good';
this.setState({ mood: newMood });
}

render() {
return (
<div>
<h1>I'm feeling {this.state.mood}!</h1>
<button onClick={this.toggleMood}>Click Me</button>
</div>
);
}
}

const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<Mood />);

當你透過 this 來定義事件處理器(event handler)時,記得要在建構式中加上 this.methodName = this.methodName.bind(this)。若想瞭解背後的理由,可參考 Handling Events @ React。

如果不想使用在 constructor 中去綁定 this

使用 public class fields 語法
class LoggingButton extends React.Component {
// This syntax ensures `this` is bound within handleClick.
// Warning: this is *experimental* syntax.
handleClick = () => {
console.log('this is:', this);
};

render() {
return <button onClick={this.handleClick}>Click me</button>;
}
}
在綁定事件的地方使用箭頭函式
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}

render() {
// This syntax ensures `this` is bound within handleClick
return <button onClick={(e) => this.handleClick(e)}>Click me</button>;
}
}

這種作法的問題在於,每次轉譯 <LoggingButton /> 時,都會有新的回呼函式(callback)被建立,在大部分的情況下這麼做是沒問題的,然而如果回呼函式是透過 prop 的方式傳遞到子層元件時,可能會導致這些元件執行額外不必要的重新轉譯(re-rendering)。因此官方建議還是在建構式中透過 bind 綁定,或使用 class fields 語法來避免可能的效能問題。

官方並不建議這種作法

Presentational Component 和 Display Component

在 React 當中,要時刻提醒自己不要讓單一個元件做太多不同的功能,而 Separating container components from presentational components 可以幫助我們思考該元件需不需要拆開,以及如何進行這個分割。

基本的原則是:「如果一個元件中有 state,而且又會根據 props 進行計算,或者要管理複雜的邏輯,那麼這個元件就應該同時轉譯 HTML-like JSX。

不要轉譯 HTML-like JSX 時,這個元件只需要轉譯其他元件,而被它轉譯的主要任務就是轉譯 HTML-like JSX。這也是將商業邏輯從表現層中抽離出來(separates your business logic from your presentational logic)的設計模式。

presentational 元件中,通常只會看到 render 而不會看到其他的功能。

Stateless Functional Component

如果你有一個元件裡面只有 render 函式,那麼你可以不用使用 React.Component 而是可以完全使用 JavaScript 的函式,這樣的函式稱作 stateless function component,舉例來說:

// 原本的 presentational 元件(只有 render function 在內)
export class MyComponentClass extends React.Component {
render() {
return <h1>{this.props.title}</h1>;
}
}

改成 stateless function component 後:

// Stateless functional component way to display a prop:
export const MyComponentClass = (props) => {
return <h1>{props.title}</h1>;
};

在 stateless functional components 中,通常會代入 props,要取用這些 props 只需要在 stateless functional component 中代入參數,這些參數即會自動等同於 props

Controlled vs Uncontrolled

Controlled and uncontrolled form inputs in React don't have to be complicated

在 React 的 uncontrolled component 中,資料狀態是保存在 DOM 元素上,而不是由 React 來控制和管理;相反地,如果是 controlled component,所有表單的資料狀態都會透過 React 所控制。

uncontrolled component

想想 <input type='text' /> 這個元素,當你在對話框輸入內容時,可以透過 input 元素的 value 屬性來取得當前輸入框內的內容,也就是說 <input type='text' /> 時刻都追蹤著它裡面的內容,並且可以回傳給你,這個 input 的 value 是保存在 DOM 元素內,而非透過 React 來管控,這時就屬於 uncontrolled component

controlled component

在 React 中大部分的元件都屬於 controlled components。在 controlled component 中,表單的資料狀態都是透過 React 來管控(而非直接由 DOM 去取得),如果你想得知該 input 的 value,必須透過 state 得知。

當你對 <input /> 給予 value 屬性時,這個 <input /> 就變成了 controlled,它不再會使用 DOM 內部的資料狀態,而這也是在 React 中較常所用的方式。

信息

由於一旦對 <input /> 加上 value 屬性後,該表單元素即會變成 controlled component,因此若是想針對 uncontrolled component 設定預設值的話,可以使用 default values 這個屬性。

父元件與子元件(parent component & child component)

在元件中載入其他元件

ProfilePage 將會轉譯 NavBar

要被載入的元件,需要在 class 關鍵字之前使用 export

// ./NavBar.js
import React from 'react';

export class NavBar extends React.Component {
render() {
const pages = ['home', 'blog', 'pics', 'bio', 'art', 'shop', 'about', 'contact'];
const navLinks = pages.map((page) => {
return <a href={'/' + page}>{page}</a>;
});

return <nav>{navLinks}</nav>;
}
}

要載入其他元件的元件,使用 import 載入其他元件之後,即可在 render function 中使用 <NavBar />

// ./ProfilePage.js
import React from 'react';
import ReactDOM from 'react-dom';
import { NavBar } from './NavBar';

class ProfilePage extends React.Component {
render() {
return (
<div>
<NavBar />
<h1>All About Me!</h1>
<p>I like movies and blah blah blah blah blah</p>
<img src="https://s3.amazonaws.com/codecademy-content/courses/React/react_photo-monkeyselfie.jpg" />
</div>
);
}
}

在這個例子中,我們會稱 ProfilePage 將會轉譯 NavBar(it follows that <ProfilePage />is going to render <NavBar />.)

將資料透過 props 代入元件內

一個元件的 props 屬性會包含所有該元件的資訊,在 render 函式中可以透過 this.props 取得這個屬性:

import React from 'react';
import ReactDOM from 'react-dom';

// STEP 2: 在 render function 中可以透過 this.props 取得資料
class Greeting extends React.Component {
render() {
return <h1>Hi there, {this.props.town}!</h1>;
}
}

// STEP 1: 在 render 中放入 props
const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(
<Greeting
name="Aaron"
town="Taipei"
age={2}
haunted={false}
myInfo={['top', 'secret', 'lol']}
/>
);

Props 和 propTypes

propTypes 是元件的類別方法,透過 propTypes 可以幫助驗證該 props 並說明該 props 的內容。

import React from 'react';
import PropTypes from 'prop-types';

// 當我們看到 this.props.message
// 可以預期會有 <MessageDisplayer message="something" /> 存在
export class MessageDisplayer extends React.Component {
render() {
return (
<div style={this.props.style}>
<h1>{this.props.message}</h1>
</div>
)
}
}

// 在 MessageDisplayer 這個類別上,會有一個名為 propTypes 的靜態屬性,在這裡可以
// 定義 props 的型別
// 定義 props 是否為必填
MessageDisplayer.propTypes = {
message: PropTypes.string,
style: PropTypes.object.isRequired,
onTodoClick: PropTypes.func.isRequired,
todos: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired
).isRequired,


// An array of a certain type
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),

// An object with property values of a certain type
optionalObjectOf: PropTypes.objectOf(PropTypes.number),

// An object taking on a particular shape
optionalObjectWithShape: PropTypes.shape({
optionalProperty: PropTypes.string,
requiredProperty: PropTypes.number.isRequired
})


};

prop-types @ Facebook Github

元件與元件溝通

父元件將資料透過 props 代入子元件內

在這個例子中,將會透過 <App /> 轉譯 <Greeting />

class 之前使用 export 將元件匯出:

// ./Greeting.js
import React from 'react';

export class Greeting extends React.Component {
render() {
return <h1>Hi there, {this.props.name}!</h1>;
}
}

透過 import 載入元件,並透過 <Greeting name="Foobar"> 將父層的資料代入子層內:

// ./App.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Greeting } from './Greeting';

class App extends React.Component {
render() {
return (
<div>
<Greeting name="FooBar" />
</div>
);
}
}

const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<App />);

設定 props 的預設值

keywords: .defaultProps

可以使用 ClassName.defaultProps 這個屬性來為元件的 props 設定預設值,當 <Button /> 中沒有代入 props 就會使用 defaultProps 的內容當作它的預設值:

import React from 'react';
import ReactDOM from 'react-dom';

class Button extends React.Component {
render() {
return <button>{this.props.text}</button>;
}
}

// defaultProps goes here:
Button.defaultProps = {
text: 'I am a button',
};

const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<Button />);

子元件更新父元件的資料狀態

步驟一:在父元件撰寫修改資料狀態的事件

在父元件撰寫 handleClick() 這個可以修改資料狀態(state)的事件:

// ./ParentClass.js
import React from 'react';
import ReactDOM from 'react-dom';
import { ChildClass } from './ChildClass';

class ParentClass extends React.Component {
constructor(props) {
super(props);

this.state = { totalClicks: 0 };
this.handleClick = this.handleClick.bind(this);
}

// 步驟一:在父元件撰寫修改資料狀態的事件
handleClick() {
const total = this.state.totalClicks;

this.setState({ totalClicks: total + 1 });
}
}

步驟二:將父元件的事件傳遞給子元件

父元件將事件可以改變其資料狀態的 handleClick 傳送到子元件內:

// ./ParentClass.js
import React from 'react';
import ReactDOM from 'react-dom';
import { ChildClass } from './ChildClass';

class ParentClass extends React.Component {
constructor(props) {
super(props);

this.state = { totalClicks: 0 };

this.handleClick = this.handleClick.bind(this);
}

handleClick() {
const total = this.state.totalClicks;

this.setState({ totalClicks: total + 1 });
}

// 父元件(stateful component)將可以改變其資料狀態的 handleClick 事件傳送到子元件(stateless component)內
render() {
return <ChildClass onClick={this.handleClick} />;
}
}

步驟三:在子元件中呼叫父元件傳遞進來的事件

在子元件 <ChildClass /> 中使用父元件 <ParentClass /> 傳遞進來的 onClick 事件:

// ./ChildClass.js
import React from 'react';
import ReactDOM from 'react-dom';

export class ChildClass extends React.Component {
render() {
return (
// 在子元件(stateless)中使用透過父元件傳遞進來的 onClick 事件
<button onClick={this.props.onClick}>Click Me!</button>
);
}
}

範例程式碼:父元件的事件方法傳遞給子元件(event handler as prop)

在這個例子中,將把事件從父元件(<Talker />)傳到子元件(<Button />)中。

在父元件 <Talker /> 中,在 render 函式中把事件處理器(event handler)傳遞進去。在這裡需要留意的是,在 <Talker /> 元件中,render 函式式中使用了 <Button onClick={...} />,這裡的 onClick 只是一個 props 名稱,而不是綁定事件的意思。

因為 <Button /> 是一個元件實例(component instance),而不是 JSX 元素(HTML-like JSX element),所以 onClick 指的會是 props,而非綁定事件;只有在 JSX 元素上使用 onClick 才會事件綁定。

// ./Talker.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Button } from './Button';

class Talker extends React.Component {
clickHandle() {
let speech = '';
for (let i = 0; i < 10000; i++) {
speech += 'blah ';
}
alert(speech);
}

render() {
// 在元件實例上,這個 onClick 是一個屬性,而不是綁定事件的意思
return <Button onClick={this.clickHandle} />;
}
}

const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<Talker />);

在子元件(<Button />)中,可以透過 this.props.talk 取得父元件傳進來的 event handler:

// ./Button.js
import React from 'react';

export class Button extends React.Component {
render() {
return <button onClick={this.props.onClick}>Click me!</button>;
}
}
事件處理器方法的命名

在傳遞方法的過程中,你會看到不論是父元件或子元件,都以同樣的名稱命名它們,它們兩個不一定要是同個名稱,但是慣例上來說,我們會用相同的名稱來命名。

慣例上,可以先思考這個事件的類型是什麼,如果是點擊(click),可以命名為 handleClick;如果是打字(keyPress)事件,可以命名為 handleKeyPress

範例程式碼:子元件修改父元件資料狀態

Parent.js
// ./Parent.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Child } from './Child';

class Parent extends React.Component {
constructor(props) {
super(props);

this.state = { name: 'Frarthur' };

// STEP 2:將 changeName 方法的 this 綁定
this.changeName = this.changeName.bind(this);
}

// STEP 1:撰寫可以改變 state 的方法
changeName(newName) {
this.setState({
name: newName,
});
}

render() {
// STEP 3:把 changeName 方法代入子元件中
return <Child name={this.state.name} onChange={this.changeName} />;
}
}

const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<Parent />);
Child.js

原本可以直接取用父層透過 props 傳遞進來的 onClick 屬性,但因為在這裡需要拿到 event 屬性的內容,因此在子元件中額外定義一個 handleChange 方法來取得 event 屬性:

// ./Child.js

import React from 'react';

export class Child extends React.Component {
constructor(props) {
super(props);
// STEP 2:將 handleChange 方法的 this 綁定
this.handleChange = this.handleChange.bind(this);
}

// STEP 1:建立可以取得 event 屬性的方法
handleChange(e) {
const name = e.target.value;
this.props.onChange(name);
}

render() {
return (
<div>
<h1>
Hey my name is {this.props.name}!
</h1>
<!-- STEP 3:在 onChange 事件中呼叫 handleChange -->
<select id="great-names" onChange={this.handleChange}>
<option value="Frarthur">
Frarthur
</option>

<option value="Gromulus">
Gromulus
</option>

<option value="Thinkpiece">
Thinkpiece
</option>
</select>
</div>
);
}

元件內的 children 屬性

在每一個 props 屬性中,都包含有一個 children 的屬性,這個 this.props.children 會回傳其父層元件中 JSX 標籤(JSX tags)內的內容。

在這個例子中將會透過 <App /> 轉譯出 <List />

<App /> 內,並非直接使用 <List /> ,而是使用 <List> ... </List> 來把 <li></li> 包在裡面:

// ./App.js
import React from 'react';
import ReactDOM from 'react-dom';
import { List } from './List';

class App extends React.Component {
render() {
return (
<div>
<List type="Living Musician">
<li>Mars</li>
<li>Harvey Sid Fisher</li>
</List>
<List type="Living Cat Musician">
<li>Nora the Piano Cat</li>
</List>
</div>
);
}
}

const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<App />);

在子元件 <List /> 中,可以使用 this.props.children 就可以取得在父元件中 <List> ... </List>... 的部分:

// ./List.js
import React from 'react';

export class List extends React.Component {
render() {
let titleText = `Favorite ${this.props.type}`;
if (this.props.children instanceof Array) {
titleText += 's';
}
return (
<div>
<h1>{titleText}</h1>
<ul>{this.props.children}</ul>
</div>
);
}
}

表單(Two Way Binding)

import React from 'react';
import ReactDOM from 'react-dom';

export class Input extends React.Component {
constructor(props) {
super(props);

this.state = {
userInput: ''
};

// STEP 3
this.handleUserInput = this.handleUserInput.bind(this);
}

// STEP 2: handleUserInput 改變 state
handleUserInput(e) {
this.setState({
userInput: e.target.value
});
}

render() {
return (
<div>
<input
// STEP 1: onChange 時呼叫 handleUserInput
onChange={this.handleUserInput}
type="text"
// STEP 4: 把 value 綁定回來
value={this.state.userInput}
/>
<!-- // STEP 5: Two Way Binding -->
<h1>{this.state.userInput}</h1>
</div>
);
}
}

const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<Input />);

常見問題清單

參考