[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 元件中有兩種方式可以存取動態的資料,分別是 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>;
}
}