[react] 元件(component)
未閱讀:Writing Resilient Components @ OverReacted
本文以 Class Component 為主要範例。自 React 16.8 起,Function Component 搭配 Hooks 已成為撰寫 React 元件的標準方式,不再需要繼承 Component class。建議搭配 React Hooks 一起閱讀。
建立元件(Component)
React 中的元件(component)是一個小而可重複使用的程式碼。在早期版本中,元件需要從 Component 這個類別(class)而來,但自 React 16.8 起,可以透過 Function Component 搭配 Hooks 來建立元件,這也是目前推薦的做法。以下的 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 元件中有兩種方式可以存取動態的資料,分別是 props 和 state。與 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 />);
常見問題清單
- React ES6 class constructor super():說明什麼時候要用
super(),什麼時候要用super(props),什麼時候不用
參考
- Learn React JS: Part I @ Codecademy