owned this note
owned this note
Published
Linked with GitHub
# React.js 基本概念與引入 CSS 的方式
###### tags: `javascript`、`react`
[reference]: https:// "React.js 簡介與環境建置"
## 前情提要
如果還沒建置 React 的環境,請參考[這篇文章](https://hackmd.io/c9_NvnJkROyUkJoKSCgj5A)
> 注意!下面的程式碼幾乎都省略了開頭的 `import React, { Component } from 'react'` 和 結尾的 `export default App`
## Component 與 render function 的交織
在 React 當中,每個東西都是 Component,最後呈現的前端畫面都是 Component 合併在一起的結晶。你也可以在一個 Component 裡面放另外一個 Component,如下所示:
```jsx=
import React, { Component } from 'react'
class Text extends Component {
render() {
return (
<p>Hi,I'm a text</p>
)
}
}
class Title extends Component {
render() {
return (
<h1>The Title man is me</h1>
)
}
}
class App extends Component {
render() {
return (
<div>
<Title />
<Text />
</div>
)
}
}
export default App
```
在上面的例子可以看到在 `App` 這個 Component 中,又加了 `<Title />` 與 `<Text />` 兩個標籤,這兩個分別代表了 `Title` 和 `Text` 的 Component,而整個執行順序是當遇到了 `App` 這個 Component(假設在 `index.js`檔案(這個是 webpack 打包的 entry 檔案)只有設定引入 `App` Component)就開始 render 它的畫面,然後接著遇到了 `<Title/>` 就 render `Title` 這個 Component 的畫面;以此類推。
## JSX 語法
在上面 Component 的那段程式碼當中,我們可以看到在 `App` 的 Component 裡,`return` 回去要 render 出來的 HTML 語法不是正規的 HTML 語法:
```jsx=
<div>
<Title />
<Text />
</div>
```
雖然 HTML 實際上也可以使用這樣的方式來建構,但是各方面功能的支援度並不高。
而上面這樣的寫法被稱作「JSX」,可以把它看作 HTML 與 JavaScript 的合體語法,但它既不是 HTML 也不是一般的字串,有興趣研究的可以到 React 官網閱讀 JSX 的詳細介紹:[Introducing JSX](https://reactjs.org/docs/introducing-jsx.html)
#### 新增 class 屬性
在 JSX 語法,如果要對標籤新增 `class` 屬性的話,需要輸入 `className` 而不是 `class`:
```jsx=
<div className='Title'>
<Title />
<Text />
</div>
```
#### 放入 JavaScript 的變數到 JSX
我們可以在 JSX 的語法當中放入所建立好的 JavaScript 變數,像下面這樣:
```jsx=
class App extends Component {
render() {
const titleClass = 'title';
return (
<div className={titleClass}>
<Title />
<Text />
</div>
)
}
}
```
在 `const titleClass = 'title';` 這邊我們定義了 `titleClass` 這個變數的值是 `title`。
而在 JSX 的語法當中,我們可以使用 `{}` 大括弧的用法,在大括弧裡面放入 JavaScript 的變數,這樣就可以把變數的值給帶進去 JSX 語法了,如上面例子的 ` <div className={titleClass}>`,而這個用法就像是在 JavaScript 的字串當中要放入變數的概念一樣:`<div class=${titleClass}`
,只是語法上有所差異。
另外在標籤裡面新增 CSS 屬性在 HTML 的預設當中必須使用 `style` 屬性,在 JSX 裡也是一樣的,不過帶入的值要參照上面所說的 `{}` 大括弧格式,像是這樣:
```jsx=
class App extends Component {
render() {
const titleClass = 'title';
const styleAttr = {
color: 'red',
}
return (
<div className={titleClass} style={styleAttr}>
<Title />
<Text />
</div>
)
}
}
```
`styleAttr` 的值就是要帶入 JSX 語法中的 CSS 屬性,當然你也可以直接這樣寫就不必另外新增一個變數:
```jsx=
class App extends Component {
render() {
const titleClass = 'title';
return (
<div className={titleClass} style={{
color: 'Green',
}}>
<Title />
<Text />
</div>
)
}
}
```
要記得在大括弧裡面再加一次大括弧(因為 CSS 屬性新增時所需)。
#### CSS 屬性駝峰式命名
另外,在 CSS 的屬性中,有些屬性名稱中間是有 `-` 的符號,像是 `font-size`,但是如果加在 JSX 的語法裡,這樣的用法是錯誤的,要以駝峰式的命名才行,如下:
```jsx=
class App extends Component {
render() {
const titleClass = 'title';
return (
<div className={titleClass} style={{
color: 'Green',
fontSize: '50px', // 駝峰式命名
}}>
<Title />
<Text />
</div >
)
}
}
```
#### return 的寫法
拿上面的例子來說:
```jsx=
class App extends Component {
render() {
const titleClass = 'title';
return (
<div className={titleClass} style={{
color: 'Green',
fontSize: '50px', // 駝峰式命名
}}>
<Title />
<Text />
</div >
)
}
}
```
在 `return` 的地方會看到頭跟尾有加 `()` 的符號,主要原因在於想要回傳的內容並沒有與 `return` 同一行,因此必須加上 `()` 的符號,否則會 `return` 失敗,但如果欲回傳的內容的第一行和 `return` 在同一行就不需要加了,像下面這樣:
```jsx=
class App extends Component {
render() {
const titleClass = 'title';
return <div className={titleClass} style={{
color: 'Green',
fontSize: '50px', // 駝峰式命名
}}>
<Title />
<Text />
</div >
}
}
```
## React 的事件機制
以往我們寫 HTML 和 JavaScript 時,語法是分開的,像是事件監聽,我們必須先建立好 HTML 的結構後,再去寫 `document.querySelector('.title').addEventListener()` 這麼一大串的語法才可以把所有功能與架構安置好。
但現在,我們使用 React,就可以藉由 JSX 的語法讓 HTML 和 JavaScript 的寫法結合起來,像是上面所說的事件監聽,在 JSX 當中我們只要像下面這麼寫,功能會是一模一樣的:
```jsx=
class Title extends Component {
render() {
function titleClick() {
alert('The Title');
}
return (
<h1 onClick={titleClick}>The Title man is me</h1>
)
}
}
```
而你也可以這麼寫(箭頭函式):
```jsx=
class Title extends Component {
render() {
return (
<h1 onClick={() => {
alert('The Title');
}}> The Title man is me</h1 >
)
}
}
```
值得注意的是,點擊的事件監聽的屬性是駝峰式的命名:`onClick()`;傳送表單的事件監聽則是 `onSubmit()`。
## state
在上述的實作當中,我們僅提到 React 的 Component 概念,接著要再加上 React 的另一個很重要的觀念,也就是 **state**(狀態),有了 state,我們就可以根據 state 的改變做出相對應的 UI 畫面呈現(Component 的不同結合方式)。
### state 的做法
最簡單在 JSX 當中使用 `state` 的做法如下:
```jsx=
class App extends Component {
state = {
counter: 1
}
render() {
return (
<div>
<h1>hello</h1>
<div>{this.state.counter}</div>
</div>
)
}
```
但是結果是失敗的,打開 DevTools,呈現的錯誤訊息是:
![new state syntax error](https://i.imgur.com/eoRUsy3.png)
上面的意思是說,所使用的 `state` 語法其實還在實驗階段,目前不能直接使用,如果要直接使用的話,必須安裝 `@babel/plugin-proposal-class-properties` 的 plugin 才能做使用。
還在實驗階段的話,我們使用最原始的做法較保險:
```jsx=
class App extends Component {
// JavaScript 的 OOP 繼承用法
constructor() {
super()
this.state = {
counter: 1
}
}
render() {
return (
<div>
<h1>hello</h1>
<div>{this.state.counter}</div>
</div>
)
}
}
```
畫面呈現就會是這樣:
![](https://i.imgur.com/slSUSEb.png)
進一步把 `hello` 字串放到 `state` 裡面:
```jsx=
class App extends Component {
constructor() {
super()
this.state = {
title: 'hello',
counter: 1
}
}
render() {
return (
<div>
<h1>{this.state.title}</h1>
<div>{this.state.counter}</div>
</div>
)
}
}
```
把 `hello` 字串加到 `this.state` 當中,再藉由 `this.state.title` 把值(`hello`)給放入 JSX 語法裡。
### setState()
如果我們要改變 `state` 當中的值,必須使用 `setState()` 的語法,如下:
```jsx=
class App extends Component {
constructor() {
super()
this.state = {
title: 'hello',
counter: 1
}
}
render() {
return (
<div>
<h1 onClick={() => {
this.setState({
counter: this.state.counter + 1
})
}}>{this.state.title}</h1>
<div>{this.state.counter}</div>
</div>
)
}
}
```
結合 `onClick()` 語法,當點擊 `<h1>` 標籤時,`state` 裡的 `counter` 就會加 1。而這個 `setState()` 的用法比較像是 HTTP Method 的 `PATCH` 而非 `PUT`,簡單說就是它只會「更新」你要更新的 `state` 值(在這個例子就是只會改到 `state` 的 `counter` 值,`title` 的值不會被改到),而不是所有的 `state` 值被你「更新的內容」所取代。
另外要注意的是,在 `onClick()` 裡面的 function 必須使用**箭頭函式**,因為如果使用 `function () {}` 匿名函式的語法的話,裡頭的 `this.setState()` 所指的 `this` 不會是 `App` 這個 Component 本身,而是 `onClick()` 這個函式,但是這個函式的 `this.setState()` 呈現的結果會是 `undefined`,很合理,因為它本身就沒有 `setState()` 這個 method 嘛。而箭頭函式比較特別的是,它會根據呼叫它的地方的上層 `this` 為它的 `this` 值,也就是箭頭函式被定義時就決定好 `this` 的值了,這點與正常使用 `this` 時的性質不太一樣,簡單講就是下方 `console.log(this)` 的值:
```jsx=
class App extends Component {
constructor() {
super()
this.state = {
title: 'hello',
counter: 1
}
}
render() {
console.log(this) // 這個
return (
<div>
<h1 onClick={() => {
this.setState({
counter: this.state.counter + 1
})
}}>{this.state.title}</h1>
<div>{this.state.counter}</div>
</div>
)
}
}
```
另外一個 setState() 的用法:
```jsx=
class App extends Component {
constructor() {
super()
this.state = {
title: 'hello',
counter: 1
}
}
handleClick() {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div>
<h1 onClick={this.handleClick}>{this.state.title}</h1>
<div>{this.state.counter}</div>
</div>
)
}
}
```
但是這樣寫百分之百是錯的,為什麼?理由跟第一個用法不能使用非箭頭函式一樣,這樣寫的話,`this.handleClick` 所指的目標(也就是在 `App` 的 `handleClick` 這個函式) 本身裡面的 `this` (也就是 `this.setState` 的 `this`)會是 `handleClick` 這個函式本身,而非 `App` 這個 Component 本身。因此我們必須再加上 `bind()` 來給定 `handleClick` 的 `this` 值固定為何:
```jsx=
class App extends Component {
constructor() {
super()
this.state = {
title: 'hello',
counter: 1
}
// 這樣 this.handleClick 回傳的值就都會是 this 了(這邊的 this 指的是 App Component)
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div>
<h1 onClick={this.handleClick}>{this.state.title}</h1>
<div>{this.state.counter}</div>
</div>
)
}
}
```
而你也可以把 `bind()`寫在 `onClick()` 裡面:
```jsx=
class App extends Component {
constructor() {
super()
this.state = {
title: 'hello',
counter: 1
}
}
handleClick() {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div>
<h1 onClick={this.handleClick.bind(this)}>{this.state.title}</h1>
<div>{this.state.counter}</div>
</div>
)
}
}
```
不過最好的做法還是上面那一個,比較方便後續其它的操作。
然而,實際上還有第三種 `setState()` 的做法:
```jsx=
class App extends Component {
constructor() {
super()
this.state = {
title: 'hello',
counter: 1
}
}
// 改成很像箭頭函式的物件(實際上叫做 Class Property)並且把函式儲存在變數當中
handleClick = () => {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div>
<h1 onClick={this.handleClick}>{this.state.title}</h1>
<div>{this.state.counter}</div>
</div>
)
}
}
```
可是這樣的做法會跑出剛才上面我們要引進 `state` 的其中一個方法有出現的錯誤:
![](https://i.imgur.com/xOp13rI.png)
一樣是叫你要安裝 `@babel/plugin-proposal-class-properties` 的 plugin,所以這個`setState()` 的方式也是還在實驗中的用法,不過是比較省事的用法。
## props 與 state 的合體技
上面提到可以在 Component 之中加上 `state`,但如果今天 Component 裡面又有另外一個 Component,那麼其它的 Component 要如何拿到使用 `state` 的 Component 的資料?像是這樣的範例:
```jsx=
class Counter extends Component {
render() {
return (
<div>test</div>
)
}
}
class App extends Component {
constructor() {
super()
this.state = {
title: 'hello',
counter: 1
}
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div>
<h1 onClick={this.handleClick}>{this.state.title}</h1>
<Counter /> // update here
</div>
)
}
}
```
`Counter` 加到 `App` 這個 Component 裡面了,那像上面所說的,`Counter` 如果也要拿到在 `App` 當中產生的 `state`,該怎麼辦?
來,可以這麼做:
```jsx=
class Counter extends Component {
render() {
return (
<div>{this.props.number}</div> // update here
)
}
}
class App extends Component {
constructor() {
super()
this.state = {
title: 'hello',
counter: 1
}
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div>
<h1 onClick={this.handleClick}>{this.state.title}</h1>
<Counter number={this.state.counter} /> // update here
</div>
)
}
}
```
在 `<Counter />` 的標籤裡面,再加上 `number={this.state.counter}`,有點像是加上了一個 attribute,然後在 `Counter` Component 裡面要獲取 `number` 的值的話,就加上 `this.props.number`。
而這整個過程,就是 React 的 `props` 機制,利用 `props` 當作媒介,去獲取在別人家所放的東西,就像是邦交國一樣,我國請一位外交官駐點(如`number`)在別人家的領事館(如 `<Counter />`),然後外交官藉由打電話或是其它方式回傳(這邊指的是 `props` 機制)別人家的訊息(如 `this.props.number`)回來我國(如`Counter` Component)。
因此使用 `props` 很輕易地就可以拿到 `App` 的 `state`(或是 function) 資料到其它 Component 上應用,這個機制可以讓相關的畫面(相關 Component)因 `state` 改變而同時更新資料,接著重新渲染 UI。
還有這種寫法:
```jsx=
class Counter extends Component {
render() {
return (
<div>{this.props.children}</div> // update here
)
}
}
class App extends Component {
constructor() {
super()
this.state = {
title: 'hello',
counter: 1
}
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div>
<h1 onClick={this.handleClick}>{this.state.title}</h1>
<Counter>
{this.state.counter} // update here
</Counter>
</div>
)
}
}
```
把 `Counter` 當作一般的標籤一樣,在標籤之間的所有內容,會回傳到 `this.props.children` 當中,而 `children` 就是表示在標籤之間的所有內容。
也可以回傳非 `state` 的內容,直接在 `App` Component 建立 `Counter` Component 的內容:
```jsx=
class Counter extends Component {
render() {
return (
<div>{this.props.children}</div>
)
}
}
class App extends Component {
constructor() {
super()
this.state = {
title: 'hello',
counter: 1
}
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div>
<h1 onClick={this.handleClick}>{this.state.title}</h1>
<Counter>
// update here
<a href="http://www.google.com">
{'counter hi'}
</a>
</Counter>
</div>
)
}
}
```
畫面就是這樣:
![](https://i.imgur.com/ztj9x3f.png)
## functional Component
除了 `class Component` 之外,其實還有 `functional Component`,如果 `class Component` 長這樣:
```jsx=
class Counter extends Component {
render() {
return (
<div>{this.props.number}</div>
)
}
}
```
那麼 `functional Component ` 就是長這樣:
```jsx=
function Counter(props) {
return (
<div>{props.number}</div>
)
}
```
可以藉由參數放入 `props` 當作橋梁來獲取在其它 Component 上面在 `Counter` 標籤的值。
而你也可以使用 ES6 的解構語法:
```jsx=
function Counter(props) {
const { number } = props
return (
<div>{number}</div>
)
}
```
還有更偷懶的方式:
```jsx=
function Counter({ number }) {
return (
<div>{number}</div>
)
}
```
也可以寫成箭頭函式:
```jsx=
const Counter = ({ number }) => {
return (
<div>{number}</div>
)
}
```
那,`class Component` 和 `functional Component` 究竟有什麼不同呢? 簡單來說,`functional Component` 做得到的事,`class Component` 都做得到,但是反過來說,`functional Component` 不能使用 `state`,因為沒有繼承 Component 的 class,自然不會有 `this`、`constructor` 那些用法,甚至類似的其它功能 `functional Component` 都沒辦法使用。
那麼使用 `functional Component` 的時機為何?**只要你只是單純想要在 UI 的某一塊加上幾行元素(標籤)並且不使用其它功能,那就可以用 `functional Component`** 來寫會比較簡潔與便利,畢竟每次寫 `class` 都要一大串,要寫 `extends` 還要 `render()` 有的沒的。
## Component 之間的溝通流程
以這個例子為例:
```jsx=
const Counter = ({ number }) => {
return (
<div>{number}</div>
)
}
const Title = (props) => {
return (
<h1>{props.text}</h1>
)
}
class App extends Component {
constructor() {
super()
this.state = {
title: 'hello',
counter: 1
}
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div>
<Title text={this.state.title} />
<Counter number={this.state.counter} />
</div>
)
}
}
```
如果我們要在畫面按下 `Title` 標籤的內容的時候可以讓 `App` 之中的 `state` 的 `counter` 加 1 該如何做?這牽涉了 Component 之間傳遞的問題。而在這個例子當中,我們稱呼 `App` 為 Parent Component;`Title` 和 `Counter` 為 Children Component,現在我們要解決的目標是,在 `Title` 這個 Children Component 觸發事件的時候,要能傳遞並反應到 Parent Component 的 `state` 當中。
可以這麼做:
```jsx=
const Counter = ({ number }) => {
return (
<div>{number}</div>
)
}
// update here
const Title = (props) => {
return (
<h1 onClick={props.handleClick}>{props.text}</h1>
)
}
class App extends Component {
constructor() {
super()
this.state = {
title: 'hello',
counter: 1
}
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div>
// update here
<Title handleClick={this.handleClick} text={this.state.title} />
<Counter number={this.state.counter} />
</div>
)
}
}
```
在 Parent Component(`App`)當中的 Children Component(`<Title/>`)標籤加上 `handleClick` 的 function,然後在 Children Component(`Title`)加上事件監聽 `onClick()` 並且藉由 `props` 指向 `handleClick` 這個 function。
整個傳遞的流程是這樣:
1. 先 render `Title` 的 `text` 值(hello),並且加上 `handleClick` 這個 function 和 render `counter` 的 `number` 值(1)。
2. 當觸發 `Title` 在 `<h1>` 的 `onClick()` 事件監聽時,等同觸發(或者是呼叫)了 `props.handleClick` ,也就是在 `App` component 的 `hanldeClick` funciton。
3. `handleClick` 被呼叫後,執行 `setState()` 讓 state 的 `counter` 的值加 1,來到了 2。
4. re-render `Title` 的 `text` 值(hello),並且加上 `handleClick` 這個 function(不過 React 很聰明的是沒有改變過的資料或 DOM 節點就不會重新 render)。
5. re-render `counter` 的 `number` 值(2)。
以上就是 Component 之間的傳遞過程,其中最關鍵就在第 2 的步驟。
你也可以在 `props.handleClick` 加上參數,像這樣:`props.handleClick(123)`,那麼那個 `123` 的引數就會傳到被呼叫的 function 的參數當中(但這個例子的 function 沒有參數,只是舉例)。
## React 加上 CSS 的各種用法(門派)
### 第一種:按照慣例
最傳統的用法,也就是直接在 HTML 檔案引入 CSS 檔案:
```
<link rel='stylesheet' type='text/css' href='./style.css' />
```
### 第二種:css-loader、style-loader
藉由安裝兩個與 webpack 有關的 modules 引入 webpack 流程,就可以在 React 當中負責呈現畫面的 JavaScript 檔案直接 `import` CSS 檔案,這個概念有點像是第一種方法(在 HTML 引入 CSS 檔案),不過比較方便的點是這個方法在 webpack 流程並借助 React 的 library ,因此可以即時呈現更新的畫面。
首先安裝 `css-loader` 和 `style-loader`:`npm install css-loader style-loader`
接著,在 `webpackage.config.js` 檔案加上 `css-loader` 和 `style-loader`:
```javascript=
module: {
rules: [
{
test: /\.css$/,
loader: ['style-loader', 'css-loader']
}
]
}
```
這邊的 `loader` 還有另外一個較正規的寫法:
```javascript=
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
}
]
}
```
值得注意的是,`loader` 的**執行順序為由右到左,或是由下到上**,在這個例子當中,`css-loader` 需要先被執行才是正確的執行順序。
接著在 `app.js` 檔案(也就是藉由 React 語法負責呈現 UI 的 JavaScript 檔案)中新增:`import "./style.css"`,這個 `style.css` 就是要引入的 CSS 檔案,接著 `npm run start` 查看是否有成功。
### 第三種:sass-loader
也就是在 webpack 的流程再加上 CSS preprocessor 的處理,只要安裝 `sass-loader` 後,並且做一些和第二種方式類似的設定調整,就可以使用了,詳情請看 [sass-loader](https://www.npmjs.com/package/sass-loader)
### 第四種:styled-components
https://www.styled-components.com/
這是個很特別的方式,特別在於,你可以在 `app.js` 當中自定義標籤名稱,並且設定 CSS 屬性,同時也支援根據不同 `props` 產生不同的 CSS 屬性。
首先先安裝 `styled-components`:`npm install styled-components`。
安裝完畢後就可以直接在任何 JavaScript 檔案引入這個 module,我們引入到 `app.js` 檔案:
```jsx=
import styled from 'styled-components'
const Header = styled.h1`
color: red;
font-size: 20px;
`
const Title = (props) => {
return (
<Header onClick={props.handleClick}>{props.text}</Header>
)
}
```
第 1 行引入 `styled-components` 的 `styled` 用法,就可以開始自定義標籤,第 3 行的 `Header` 就是自定義所有的 `<h1>` 標籤在 `app.js` 的檔案取代為 `Header` 名稱並且擁有 `color: red; font-size: 20px;` 的 CSS 屬性。
因此你可以在第 10 行看到標籤由原本的 `<h1>` 改為 `<Header>` 了,但本質上 render 出的 HTML 結構它還是 `<h1>` 標籤,只不過是在 JavaScript 檔案定義的名稱不同。
### 第五種:每個與 JS 檔案搭配的 CSS 檔案,屬性命名不衝突,並自動編譯 SCSS 檔成 CSS 檔
按照參考資料的文章安裝
`npm install node-sass`
`npm install style-loader css-loader sass-loader --save-dev`
`npm install mini-css-extract-plugin --save-dev`
**注意**:
在開發時,更改 SCSS 的語法不會同步更新,解決方法:
[webpack live hot reload for sass
](https://stackoverflow.com/questions/52043727/webpack-live-hot-reload-for-sass/52045575)
#### 參考資料
[SCSS 加上 Webpack 混搭款,讓你寫 CSS 上天堂](https://ithelp.ithome.com.tw/articles/10215681)
</br>
對於每個 CSS 的使用方式各有千秋,各有門派,可以自行決定哪一套比較適合你,比較好上手。