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) 、ListItem 、ListItemText ,這樣的使用者介面比較算是典型的手機介面。
先定義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={{
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
參考資料
Components in 專題知識分享
React | 為了與 Hooks 相遇 - Function Components 升級記
useState
useEffect
Cool kids handle state with Hooks
Using a class component
Using a functional component
Using the useState hook
Using the useReducer hook