Functional Component

Functional Component

2021/07/07 (補充內容)
2021/10/06 (更新內容)

Functional Component

React的元件有兩種形式,過去是以Class Component的方式開發,在react 16.8之後,新增了React Hooks,也因為Functional Component相對精簡,使得Functional Component的開發方式成為主要的選項。

在react裡,我們盡量把程式切割為不同的元件,利用元件的切割,讓程式碼可以重複使用,也避免讓元件過於複雜、難以理解。首先,先定義一個空的function。

在App.js的同一個檔案夾裡,新增 Click.js:

import React from 'react';


export default function Click() {


}

接下來,就直接定義function的回傳值,我們定義一個count的變數,點擊button後加一。我們盡量把程式邏輯與畫面切割,盡量不要把程式碼放在JSX裡。例如,我們把點選按鈕之後的程式邏輯放在handleClick()裡。

import React from 'react';

export default function Click() {

let count = 0;

const handleClick = function() {

alert(count);

count++;

}

return (

<button onClick={handleClick}>click me</button>

);

}

我們在App.js裡把這個元件加進去:

import React from 'react';

import './App.css';

import Click from './Click';


function App() {

return (

<div className="App">

<header className="App-header">

<Click/>

</header>

</div>

);

}


export default App;

可以看到變數會更動。

functional component其實雖然是function,但是行為跟class滿像的,當我們在app.js裡定義了兩個Click的時候,你會發現,兩個Click裡的count是獨立的。

import React from 'react';

import './App.css';

import Click from './Click';


function App() {

return (

<div className="App">

<header className="App-header">

<Click/>

<Click/>

</header>

</div>

);

}


export default App;

接下來,如果我們想把count的內容放在button,程式可以改為:

import React from 'react';

export default function Click() {

let count = 0;

const handleClick = function() {

alert(count);

count++;

}

return (

<button onClick={handleClick}>{count}</button>

);

}

執行之後,handleClick裡的count會增加,但是,button裡的數字卻不會變動。為什麼呢? 主要的原因是react為了效率,不會不斷的重新執行return產生(render)網頁內容。 在元件裡,要讓react重新執行return的內容,就需要利用state。當state被更動時,才會重新執行return產生(render)網頁內容。

hooks

useState

在沒有react hooks之前,元件如果需要儲存資料,就必須要採用Class Component,在react 16.8之後,就可以利用react hooks的useState(),可以讓Functional Component也能有State了 (詳參: React | 為了與 Hooks 相遇 - Function Components 升級記React.js: Functional Components vs Class-based Components )。

首先,先要import useState,注意,import的時候要加大括號。

import React, {useState} from 'react';

hook的語法是先定義一個陣列,第一個是變數名稱,第二個是設定變數的方法名稱,這樣的JavaScript 語法叫做 array destructuring (詳參:array )。useState先接收一個參數,這個參數會是state的起始值,useState會回傳一個變數以及一個改變變數的方法。以下面的例子而言,count是state的名稱,初始值是0,可利用setCount改變count的內容。(詳參: 使用 State Hook)

const [count, setCount] = useState(0);

比起class component起來,程式碼簡潔多了! 不過,目前網路上多數的教材還是以Class Component居多,所以,還是要了解Class Component的寫法以及兩種寫法間的差異,才知道將class component的範例改為functional component。

要注意的是,state的操作不是馬上生效(非同步/asynchronous)的。如果在setCount中利用alert顯示count的內容,會發現,count還沒加一,這就是非同步的現象,也就是setCount並不會馬上生效,其實是在handleClick執行完,要重新顯示頁面前才會完成更新的動作。

import React, {useState} from 'react';

export default function Click() {


const [count, setCount] = useState(0);


const handleClick = function() {

setCount(count+1);

alert(count);

}


return (

<button onClick={handleClick}>{count}</button>

);

}

Click.js的完整的程式:

import React, {useState} from 'react';

export default function Click() {


const [count, setCount] = useState(0);


const handleClick = function() {

setCount(count+1);

//alert(count);

}


return (

<button onClick={handleClick}>{count}</button>

);

}

useEffect

在react裡,在state變數或props變動時,會自動觸發rerender(畫面重整),如果我們希望在state變數或prpos變動時,就去執行某些動作,在functional component裡,可以使用useEffect hook。useEffect接受一個參數,一般稱這個參數為call back function,也就是告訴react,在每一次render被觸發之後,就去呼叫showCount。

**在useEffect所呼叫的函數裡,不要使用alert,否則會有:

[Violation] 'message' handler took 9085ms

接下來,相關的動作就改為console.log。

我們改一下程式,加了useEffect,並且去呼叫showCount,showCount取得最新的count內容,這時候,每次點選按鈕會呼叫handleClick,handleClick會更動count的內容,並且畫面會重整,重整後,呼叫useEffect,啟動showCount,就會在console看到count內容

import React, {useState, useEffect} from 'react';

export default function Click() {


const [count, setCount] = useState(0);


const handleClick = function() {

setCount(count+1);

}


const showCount = function(){

console.log(count);

}


useEffect(showCount);


return (

<button onClick={handleClick}>{count}</button>

);

}

如果兩個state變數被更動時,需要執行不同的function,就可以有兩個useEffect。

import React, {useState, useEffect} from 'react';

export default function Click() {


const [count, setCount] = useState(0);

const [count2, setCount2] = useState(0);


const handleClick = function() {

setCount(count+1);

}


const handleClick2 = function() {

setCount2(count2+1);

}


const showCount = function(){

console.log(count);


}

const showCount2 = function(){

console.log(count2);


}


useEffect(showCount, [count]);

useEffect(showCount2, [count2]);


return (

<div>

<button onClick={handleClick}>{count}</button>

<button onClick={handleClick2}>{count2}</button>

</div>

);

}

** 注意 ** 在useEffect裡 (或者呼叫的函數裡),更動state變數時要小心,避免產生無窮迴圈。

作業

  • 試試看如果是每次按按鈕,不是將count加一,而是新增一個0到10的整數亂數到陣列最後,並將陣列顯示在頁面上,要如何改寫?

    • 利用javascript產生亂數可參考JavaScript Random

    • ** 注意 ** 當state內容是個單純的變數時,原本的寫法不會有問題,當state的內容是陣列(或物件)時,state變數內容會有無法改變的問題

  • How to Add to an Array in React State using Hooks

    • 使用concat,而不用push也可以解決

    • The reason .concat() works to update state is that .concat() creates a new array, leaving the old array intact, and then returns the changed array.

    • On the other hand, .push() mutates the old array in place, but returns the length of the mutated array instead of the mutated array itself.

    • Using a wrapper function inside the React Hooks setter function is a better solution

// Using .concat(), no wrapper function (not recommended)

setSearches(searches.concat(query))


// Spread operator, no wrapper function (not recommended)

setSearches([...searches, query])


// Using .concat(), wrapper function (recommended)

setSearches(searches => searches.concat(query))


// Spread operator, wrapper function (recommended)

setSearches(searches => [...searches, query])

props

在react裡,元件之間的變數是不相通的,最簡單的溝通方式是利用props,props是從母元件傳資料給子元件。假如我們要從App.js傳count到Click.js

import React from 'react';

import logo from './logo.svg';

import './App.css';

import Click from './Click';


function App() {

return (

<div className="App">

<header className="App-header">

<Click count={10}/>

</header>

</div>

);

}


export default App;

在Click.js裡,利用function接收傳入的變數,我們把變數取為props,可以利用props.count,接受傳入的count。

import React, {useState, useEffect} from 'react';

export default function Click(props) {


const [count, setCount] = useState(props.count);

const handleClick = function() {

setCount(count+1);

}

const showCount = function(){

console.log(count);

}


useEffect(showCount);

return (

<button onClick={handleClick}>{count}</button>

);

}

props只是單方面的接受傳入的內容,並不會共用這個變數,所以,如果我們在App.js利用useEffect,會發現內容不會被更新。

import React,{useState, useEffect} from 'react';

import './App.css';

import Click from './Click';


function App() {

const [count] = useState(10);

const showCount = function(){

console.log("in App:"+count);

}

useEffect(showCount);

return (

<div className="App">

<header className="App-header">

<Click count={count}/>

</header>

</div>

);

}


export default App;

如果要讓App.js收到Click.js回傳的值,那App.js就要把更動值的方法利用props傳給Click.js

App.js

import React,{useState, useEffect} from 'react';

import './App.css';

import Click from './Click';

function App() {

const [count, setCount] = useState(10);

const updateCount = function(){

setCount(count+1);

}

const showCount = function(){

console.log("in App:"+count);

}

useEffect(showCount);

return (

<div className="App">

<header className="App-header">

<Click count={count} update={updateCount}/>

<div>{count}</div>

</header>

</div>

);

}


export default App;

click.js 可以看到點選之後,會更動App裡的count,但是,Click裡的count不會更動。因為Click裡的count在取得預設值之後,就沒有更動了。也就是useState收到App傳來的count成為預設值,App裡的count不會更動。

import React, {useState, useEffect} from 'react';

export default function Click(props) {


const [count, setCount] = useState(props.count);

const handleClick = function() {

props.update();

}

const showCount = function(){

console.log(count);

}


useEffect(showCount);

return (

<button onClick={handleClick}>{count}</button>

);

}

如果希望同步的話,就直接取props.count。

import React from 'react';

export default function Click(props) {


const handleClick = function() {

props.update();

}


return (

<button onClick={handleClick}>{props.count}</button>

);

}

** useEffect不只可以監控state變數,也可以監控props **

實作

介紹了react的基本概念後,接下來,我們來實作一下。

src/App.js

import React from 'react';

import './App.css';

import ProductList from './product/ProductList';


function App() {

return (

<div className="App">

<header className="App-header">

<ProductList/>

</header>

</div>

);

}


export default App;

src/product/ProductList.js

import React from 'react';

export default function ProductList() {


return (

<ul>

<li>iPad / 20000</li>

<li>iPhone X / 30000</li>

</ul>

);

}

將內容放到陣列裡,利用陣列的map()產生對應的jsx

import React from 'react';

export default function ProductList() {

let products= [

{desc:"iPad", price:20000},

{desc:"iPhone X", price:30000}

];


const createLi = function (product){

return <li>{product.desc}/{product.price}</li>

}


return (

<ul>

{products.map(createLi)}

</ul>

);

}

一般都會用arrow function,好處是將JSX集中在一起。arrow function對於初學者來說,不容易看懂,但是,習慣arrow function的運用之後,就會覺得滿好用的。附帶說明,arrow function這樣的語法在很多語言裡都慢慢的被納入了,例如,java 8 (詳參: Java 8: Lambda Functions—Usage and Examples)、php 7.4 (詳參: Arrow Functions)。

import React from 'react';

export default function ProductList() {

let products= [

{desc:"iPad", price:20000},

{desc:"iPhone X", price:30000}

];


return (

<ul>

{products.map(product => <li>{product.desc} / {product.price}</li>)}

</ul>

);

}

執行的時候,如果打開開發人員工具,會看到一個警告:

Warning: Each child in a list should have a unique "key" prop.

做一點小修改:

import React from 'react';

export default function ProductList() {

let products= [

{desc:"iPad", price:20000},

{desc:"iPhone X", price:30000}

];


return (

<ul>

{products.map((product, index) => <li key={index}>{index} / {product.desc} / {product.price}</li>)}

</ul>

);

}

Material-UI

Material Design是Google開發的設計語言,Material-UI就是一些Material Design的react模組/元件。各位可以使用過去慣用的Bootstrap,在react裡,可以使用React Bootstrap 。不過,我們課程帶大家使用Material-UI。目前的版本是4.12.1,第五版已經正式釋出,最新版為5.0.1。

Material-UI的官網有安裝指令,我們直接使用npm指令安裝 :

// 用npm安装

npm install @mui/material @emotion/react @emotion/styled

第四版以前的安裝方式是:

npm install @material-ui/core

請注意,第五版有滿大的更動,底層採用的元件跟第四版不太一樣。所以,參考網路上的資料的時候,要注意使用的是哪一版。

Material-UI裡有很多元件可使用,在ProductList裡可以使用List / List (API)ListItemListItemText ,這樣的使用者介面比較算是典型的手機介面

先定義List,在這裡,我們利用了兩個props: 利用subheader,給了一個header。注意,List是個react的元件,subheader是其中一個prop,aria-label是給讀螢幕程式這個元件的內容。我們可以透過props去客製元件的行為。

<List subheader="Product list" aria-label="product list">


</List>

List包含了ListItem,每個item間加了divider,ListItem也是個react的元件,divider是其中一個prop,一樣,我們可以透過props去客製元件的行為。

<List subheader="Product list" aria-label="contacts">

<ListItem divider>

</ListItem>

</List>

把內容放進ListItem

<List subheader="Product list" aria-label="product list">

{products.map((product, index) =>

<ListItem divider>

{product.desc} NT${product.price}

</ListItem>

)}

</List>

跟list一樣,如果打開開發人員工具,會發現其實是有個錯誤:

Warning: Each child in a list should have a unique "key" prop.

這是react的要求,要解決這個錯誤,請在ListItem加上key:

<List subheader="Product list" aria-label="product list">

{products.map((product, index) =>

<ListItem divider key={index}>

{product.desc} NT${product.price}

</ListItem>

)}

</List>

可以利用ListItemText,利用兩個prop:sprimary及secondary,把內容分別放在primary及secondary。

<List subheader="Product list" aria-label="product list">

{products.map((product, index) =>

<ListItem divider key={index}>

<ListItemText primary={product.desc} secondary={"NT$"+product.price}></ListItemText>

</ListItem>)}

</List>

最外面利用Box以及sx來設定style。

<Box sx={{

width: '100vw',

height: '100vh',

backgroundColor: 'background.paper',

color: 'black',

textAlign: 'left'

}}>



</Box>

完整的程式:

src/product/ProductList.js

import React from 'react';

import { Box, List, ListItem, ListItemText } from '@mui/material';

export default function ProductList() {

const products= [

{desc:"iPad", price:20000},

{desc:"iPhone X", price:30000},

];

return (

<Box sx={{

width: '100vw',

height: '100vh',

backgroundColor: 'background.paper',

color: 'black',

textAlign: 'left'

}}>

<List subheader="Product list" aria-label="product list">

{products.map((product, index) =>

<ListItem divider key={index}>

<ListItemText primary={product.desc} secondary={"NT$"+product.price}></ListItemText>

</ListItem>)}

</List>

</Box>


);

}

作業

如何記住哪個ListItem被點選了,請改寫範例,可參考Lists 中「Selected ListItem」的內容,這個練習會統整前面的useState所學到的內容。想想看,要怎麼改?

首先,你會需要,利用selectedIndex去記錄哪個item被點選:

const [selectedIndex, setSelectedIndex] = useState(0);

const handleListItemClick = (index) => {

setSelectedIndex(index);

};

接下來就考驗大家前面有沒有弄懂囉! 不過,這個部分也是一般同學最沒辦法弄懂的部分。

想想看要加甚麼內容進去,先想一下,handleListItemClick要如何呼叫? 要怎麼把index傳給handleListItemClick?

import React, {useState} from 'react';

import { Box, List, ListItem, ListItemText } from '@mui/material';

export default function ProductList() {

const products= [

{desc:"iPad", price:20000},

{desc:"iPhone X", price:30000},

];

const [selectedIndex, setSelectedIndex] = useState(-1);

const handleListItemClick = (index) => {

setSelectedIndex(index);

};


return (

<Box sx={{

width: '100vw',

height: '100vh',

backgroundColor: 'background.paper',

color: 'black',

textAlign: 'left'

}}

<List subheader="Product list" aria-label="product list">

{products.map((product, index) =>

<ListItem divider key={index}>

<ListItemText primary={product.desc} secondary={"NT$"+product.price}></ListItemText>

</ListItem>)}

</List>

</Box>


);

}

第五版後ListItem的selected不再使用 (deprecated),所以,請使用ListItemButton

<List subheader="Product list" aria-label="product list">

{products.map((product, index) =>

<ListItemButton divider key={index}>

<ListItemText primary={product.desc} secondary={"NT$"+product.price}></ListItemText>

</ListItemButton>)}

</List>

接下來,透過onClick來呼叫handleListItemClick,將index傳給handleListItemClick,才能紀錄是哪一個ListItemButton被點選 (詳參: 事件處理 )注意!! 不能寫成:

<ListItemButton divider key={index} onClick={handleListItemClick}>

</ListItemButton>)

這樣的話,handleListItemClick收不到index,因為index是在arrow function裡的變數。也不能寫成:

<ListItemButton divider key={index} onClick = {handleListItemClick(index)}>

</ListItemButton>)

要利用arrow function:

<ListItemButton divider key={index} onClick={()=>handleListItemClick(index)}>

</ListItemButton>)

這一段程式:

<List subheader="Product list" aria-label="product list">

{products.map((product, index) =>

<ListItemButton divider key={index} onClick={()=>handleListItemClick(index)}>

<ListItemText primary={product.desc} secondary={"NT$"+product.price}></ListItemText>

</ListItemButton>)}

</List>

接下來,就剩下被點選之後要將那個ListItemButton的selected設為true了~

常見問題

如果看到

Module not found: Can't resolve '@emotion/react' in 'C:\Users\USER\web2021\frontend\node_modules\@mui\styled-engine'

就是你沒有安裝@emotion/react,請記得要執行

npm install @mui/material @emotion/react @emotion/styled

參考資料