Functional Component

Functional Component

2020/09/30 (補充內容)
2020/10/6 (補充內容)
2021/05/29 (更新內容)

Functional Component

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

首先,先定義一個空的function。

import React from 'react';


export default function Click() {


}

接下來,就直接定義function的回傳值,我們定義一個count的變數,點擊button後加一。在javascript裡,定義變數可以使用let或var,建議使用let。

let count = 0;

let countString = "count:"+count;

Alert跟Button是react native裡的元件,Alert的效果跟javascript內建的alert很像,javascript的內建alert在有些手機上是不能使用的 (詳參: Alert )。

Alert.alert("count:"+count);

Button跟html裡的button也是相似,但是,要注意,在html裡是onClick,在react裡是onPress (詳參:Button )。

<Button title={countString} onPress={handleClick}/>

在click function裡又宣告了另一個handleClick function,這個function是沒有回傳值的。

function handleClick() {


}

我們設定handleClick是onPress的callback function。也就是當onPress發生時,會去呼叫handleClick。要注意,在這裡因為是設定callback function,所以,是不可以加()。如果加了(),就是將執行handleClick之後的回傳值給onPress,意義完全不同,要小心。

<Button title={countString} onPress={handleClick}/>

.src/Click.js

import React from 'react';

import {Alert, Button} from 'react-native';


export default function Click() {

let count = 0;

let countString = "count:"+count;

function handleClick() {

Alert.alert("count:"+count);

count++;

}

return (

<Button title={countString} onPress={handleClick}/>

);

}

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

import React from 'react';

import { StyleSheet, Text, View} from 'react-native';


import Click from './src/Click';


export default function App() {

return (

<View style={styles.container}>

<Text>Hello</Text>

<Click/>

</View>

);

}


const styles = StyleSheet.create({

container: {

flex: 1,

backgroundColor: '#fff',

alignItems: 'center',

justifyContent: 'center',

},

});


執行之後,handleClick裡的count會增加,但是,按鈕裡的數字卻不會變動。為什麼呢? 主要的問題就是react不會重新執行return產生(render)網頁內容。 要讓react重新執行return的內容,就需要利用state變數

hooks

useState

在沒有react hooks之前,元件如果需要state變數,就必須要採用Class Component在react 16.8之後,就可以利用react hooks的useState(),可以讓Functional Component也能有state變數了 (詳參: React | 為了與 Hooks 相遇 - Function Components 升級記React.js: Functional Components vs Class-based Components )。State變數是react component裡很特別的變數,react會監控這些變數,當這些變數被異動時,才會啟動對應的方法 。當變數內容會顯示在畫面上的時候,最好就要使用state變數

首先,先要import useState

import React, {useState} from 'react';

hook的語法是先定義一個陣列,第一個是變數名稱,第二個是設定變數的方法名稱,這樣的JavaScript 語法叫做 array destructuring。簡單的說,useState其實是回傳一個陣列,我們利用array destructuring的語法,分別命名陣列的第一個值與第二個值。也就是

const countStateVariable = useState(0); // 回傳一個陣列

const count = countStateVariable[0]; // 取陣列的第一個值

const setCount = countStateVariable[1]; // 取陣列的第二個值

這三行寫成:

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

useState先接收一個參數,這個參數會是state的起始值,useState會回傳一個變數以及一個改變變數的方法。以下面的例子而言,count是state變數的名稱,初始值是0,可利用setCount改變count的內容。(詳參: 使用 State Hook)

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

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

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

import React, {useState} from 'react';

import {Alert, Button} from 'react-native';


export default function Click() {


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

let countString = "count:"+count;


function handleClick() {

setCount(count+1);

Alert.alert("count:"+count);

}


return (

<Button title={countString} onPress={handleClick}/>

);

}

setState (setCount)接受兩種參數,第一種參數是個值,setState會更新state的內容,當我們給的值是陣列的時候,這種傳遞方式會有問題。所以,必須用第二種傳遞方式,就是接受一個callback函數,當參數是callback函數時,setState會將原有的值以及props傳給callback函數。 (詳參:State 和生命週期)

為保證可以取得count的內容,利用callback function是比較安全的作法。

function handleClick() {

setCount(prevCount => prevCount+1);

Alert.alert("count:"+count);

}

Click.js的完整的程式:

import React, {useState} from 'react';

import {Alert, Button} from 'react-native';


export default function Click() {


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

let countString = "count:"+count;


function handleClick() {

setCount((prevCount)=>prevCount+1);

Alert.alert("count:"+count);

}


return (

<Button title={countString} onPress={handleClick}/>

);

}

也可以利用arrow function來簡化這個程式,這樣子就不需要額外宣告handleClick了。

import React, {useState} from 'react';

import {Alert, Button} from 'react-native';


export default function Click() {


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

let countString = "count:"+count;


return (

<Button title={countString} onPress={()=>setCount((prevCount)=>prevCount+1)}/>

);

}

useEffect

如果我們希望在每一次render被觸發之後,就去執行某些動作,在functional component裡,可以使用useEffect hook。useEffect接受個參數,第一個參數為call back function,也就是告訴react,在每一次render被觸發之後,就去呼叫showCount。第二個參數是個陣列,這個陣列可以有多個變數,當這些變數被異動時,useEffect才會執行。如果,是一個空陣列,那就是第一次render時會呼叫,接下來就不會呼叫了。(可以在Airtable 的範例中看到使用的情境)

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

import {Alert, Button} from 'react-native';


export default function Click() {

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

let countString = "count:"+count;


function showCount(){

Alert.alert("count:"+count);

}


useEffect(showCount);


return (

<Button title={countString} onPress={()=>setCount(count+1)}/>

);

}

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

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

import {Alert, Button} from 'react-native';


export default function Click() {

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

let countString = "count:"+count;


useEffect(()=>{Alert.alert("count:"+count);});


return (

<Button title={countString} onPress={()=>setCount(count+1)}/>

);

}

props

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

import React from 'react';

import {StyleSheet, Text, View} from 'react-native';


import Click from './src/Click';


export default function App() {

return (

<View style={styles.container}>

<Text>Hello</Text>

<Click count={10}/>

</View>

);

}


const styles = StyleSheet.create({

container: {

flex: 1,

backgroundColor: '#fff',

alignItems: 'center',

justifyContent: 'center',

},

})

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

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

import {Alert, Button} from 'react-native';


export default function Click(props) {

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


let countString = "count:"+count;


function showCount(){

Alert.alert("count:"+count);

}

useEffect(showCount);

return (

<Button title={countString} onPress={()=>setCount(count+1)}/>

);

}

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

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

import {Alert, StyleSheet, Text, View} from 'react-native';


import Click from './src/Click';


export default function App() {

const [count] = useState(10);

useEffect(()=>{

Alert.alert("count in App:"+count);});

return (

<View style={styles.container}>

<Text>Hello</Text>

<Click count={count}/>

</View>

);

}


const styles = StyleSheet.create({

container: {

flex: 1,

backgroundColor: '#fff',

alignItems: 'center',

justifyContent: 'center',

},

});


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

App.js

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

import {Alert, Button, StyleSheet, Text, View} from 'react-native';


import Click from './src/Click';


export default function App() {

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

let countString = "count in App:"+count;

function updateCount(newCount){

setCount(newCount);

}

useEffect(()=>{

Alert.alert("count in App:"+count);});

return (

<View style={styles.container}>

<Text>Hello</Text>

<Button title={countString} onPress={()=>setCount(count+1)}/>

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

</View>

);

}


const styles = StyleSheet.create({

container: {

flex: 1,

backgroundColor: '#fff',

alignItems: 'center',

justifyContent: 'center',

},

});


src/click.js

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

import {Alert, Button} from 'react-native';


export default function Click(props) {

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


let countString = "count:"+count;


function showCount(){

Alert.alert("count:"+count);

props.update(count);

}

useEffect(showCount);

return (

<Button title={countString} onPress={()=>setCount(count+1)}/>

);

}

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

/App.js

import React from 'react';

import {StyleSheet, Text, View} from 'react-native';

import ProductList from './src/product/ProductList';


export default function App() {

return (

<View style={styles.container}>

<ProductList/>

</View>

);

}


const styles = StyleSheet.create({

container: {

flex: 1,

backgroundColor: '#fff',

alignItems: 'center',

justifyContent: 'center',

},

})

元件使用

FlatList

這裡,先介紹react native裡的Flatlist (詳參: FlatList )

FlatList有幾個常用的props:

  • data

    • 將要顯示的內容放在一個陣列裡,並利用data傳到FlatList裡。

  • renderItem

    • 用來處理每一筆資料,傳入一個callback function,這個callback function會收到包含三個變數的物件: data裡的每一筆資料 (item),在data裡的第幾筆(index),以及區隔(separators)。

  • keyExtractor

    • 用來設定FlatList的key,傳入一個callback function,這個callback function會收到兩個參數: data裡的每一筆資料 (item),在data裡的第幾筆(index)。因為在react native裡,key必須是字串,如果要使用index的話,要轉成字串。

renderItem會接受一個物件(data):

const renderItem = (data) => (

<View style={styles.item}>

<Text style={styles.title}>{data.item.name}</Text>

<Text>{data.item.price}</Text>

<Text>/{data.index}</Text>

</View>

);

renderItem可以利用object destructuring來簡化語法,跟array destructuring很像,只是renderItem收到的是個物件,所以,前後是{},而不是[],這個物件會包含三個變數,但是,常用的是前面兩個變數,所以,在這個範例,我們取第一個變數,命名為item,以及第二個變數,命名為index。

const renderItem = ({ item, index }) => (

<View style={styles.item}>

<Text style={styles.title}>{item.name}</Text>

<Text>{item.price}</Text>

<Text>/{index}</Text>

</View>


);

src/product/ProductList.js

import React from 'react';

import {FlatList, View, Text, StatusBar,StyleSheet} from 'react-native';


const data =[

{name:"iPhone 7", price:12000},

{name:"iPhone 8", price:10000},

]


const renderItem = ({ item, index }) => (

<View style={styles.item}>

<Text style={styles.title}>{item.name}</Text>

<Text>{item.price}</Text>

<Text>/{index}</Text>

</View>

);


export default function ProductList() {

return (

<View style={styles.container}>

<FlatList

data={data}

renderItem = {renderItem}

keyExtractor={item => item.name}

>

</FlatList>

</View>

);

}


const styles = StyleSheet.create({

container: {

//backgroundColor: '#00bfff',

flex: 1,

flexDirection: 'row',

marginTop: StatusBar.currentHeight || 0,

},

item: {

flex: 1,

flexDirection: 'row',

backgroundColor: '#00ffff',

padding: 8,

marginVertical: 8,

marginHorizontal: 16,

},

title: {

fontSize: 24,

},

});

TouchableOpacity

如果我們希望FlatList可以被點選,我們就需要使用相關的元件,例如,TouchableOpacity。

先以TouchableOpacity取代View (詳參: TouchableOpacity )

const renderItem = ({ item, index }) => (

<TouchableOpacity style={styles.item}>

<Text style={styles.title}>{item.name}</Text>

<Text>{item.price}</Text>

<Text>/{index}</Text>

</TouchableOpacity>

);

加上onPress,按了任何一個項目,在console可以看到對應的index。

const renderItem = ({ item, index }) => (

<TouchableOpacity onPress = {()=>console.log(index)} style={styles.item}>

<Text style={styles.title}>{item.name}</Text>

<Text>{item.price}</Text>

<Text>/{index}</Text>

</TouchableOpacity>

);

如果我們想更換點選項目的背景,可參考FlatList 裡的第二個範例。

先加入一個state來記住哪個項目被點選,要記得,useState要放在元件裡。

export default function ProductList() {

const [selected, setSelected] = useState(null);

renderItem也要放到元件裡,另外,我們根據index是不是等於selected,來設定背景顏色。

const backgroundColor = index === selected ? "#f9c2ff" : "#00ffff";

把背景顏色加入原本的style。

<TouchableOpacity onPress = {()=>setSelected(index)} style={[styles.item, {backgroundColor}]}>

完整的程式如下。

src/product/ProductList.js

import React, {useState} from 'react';

import {FlatList, View, Text, StatusBar,StyleSheet, TouchableOpacity} from 'react-native';


const data =[

{name:"iPhone 7", price:12000},

{name:"iPhone 8", price:10000},

{name:"iPhone X", price:20000},

]


export default function ProductList() {

const [selected, setSelected] = useState(null);

const renderItem = ({ item, index }) => {

const backgroundColor = index === selected ? "#f9c2ff" : "#00ffff";

return(

<TouchableOpacity onPress = {()=>setSelected(index)} style={[styles.item, {backgroundColor}]}>

<Text style={styles.title}>{item.name}</Text>

<Text>{item.price}</Text>

<Text>/{index}</Text>

</TouchableOpacity>

)

};



return (

<View style={styles.container}>

<FlatList

data={data}

renderItem = {renderItem}

keyExtractor={item => item.name}

>

</FlatList>

</View>

);

}


const styles = StyleSheet.create({

container: {

//backgroundColor: '#00bfff',

flex: 1,

flexDirection: 'row',

marginTop: StatusBar.currentHeight || 0,

},

item: {

flex: 1,

flexDirection: 'row',

backgroundColor: '#00ffff',

padding: 8,

marginVertical: 8,

marginHorizontal: 16,

},

title: {

fontSize: 24,

},

});

Style

首先,當我們每個元件裡都有自己的style,未來整體的style如果要調整,就會每個元件都要改,所以,建議另外產生一個styles.js

src/styles.js

import {StatusBar, StyleSheet} from 'react-native';


const styles = StyleSheet.create({

container: {

//backgroundColor: '#00bfff',

flex: 1,

//margin: 'auto',

flexDirection: 'row',

marginTop: StatusBar.currentHeight || 0,

},

item: {

flex: 1,

flexDirection: 'row',

backgroundColor: '#00ffff',

padding: 8,

marginVertical: 8,

marginHorizontal: 16,

},

title: {

fontSize: 24,

},

});

export default styles;

我們就可以共用同一個style了。

src/product/ProductList.js

import React, {useState} from 'react';

import {FlatList, View, Text, TouchableOpacity} from 'react-native';

import styles from '../styles';


const data =[

{name:"iPhone 7", price:12000},

{name:"iPhone 8", price:10000},

{name:"iPhone X", price:20000},

]


export default function ProductList() {

const [selected, setSelected] = useState(null);

const renderItem = ({ item, index }) => {

const backgroundColor = index === selected ? "#f9c2ff" : "#00ffff";

return(

<TouchableOpacity onPress = {()=>setSelected(index)} style={[styles.item, {backgroundColor}]}>

<Text style={styles.title}>{item.name}</Text>

<Text>{item.price}</Text>

<Text>/{index}</Text>

</TouchableOpacity>

)

};



return (

<View style={styles.container}>

<FlatList

data={data}

renderItem = {renderItem}

keyExtractor={item => item.name}

>

</FlatList>

</View>

);

}

前面,我們都沒有說明react native的style,在react native套用的style跟網頁使用的style其實差不多,唯一的差別是會使用camel casing,也就是"backgroundColor",而不是"background-color" (詳參: StyleHeight and WidthLayout with FlexboxColor Reference )。

在react native裡,排版是採用flexbox,所以,可以參考flexbox的相關介紹。

以flex為例,react native會把同一層的原件的flex數加總,再依flex的數字來平分空間。以現在的設定而言,因為flexDirection是row,所以,因為只有一個元件,元件會自動橫向佔滿空間,如果是多個元件,那就依flex的數字來平分空間。如果flexDirection是column,因為只有一個元件,元件則是自動縱向佔滿空間,橫向的寬度就依內容調整,如果這時候橫向要佔滿內容,就要使用alignSelf: 'stretch'。

src/styles.js

import {StatusBar, StyleSheet} from 'react-native';


const styles = StyleSheet.create({

container: {

//backgroundColor: '#00bfff',

flex: 1,

//margin: 'auto',

flexDirection: 'row',

marginTop: StatusBar.currentHeight || 0,

},

item: {

flex: 1,

flexDirection: 'row',

backgroundColor: '#00ffff',

padding: 8,

marginVertical: 8,

marginHorizontal: 16,

},

title: {

flex: 4,

fontSize: 24,

},

content: {

flex: 1,

},

});

export default styles;

renderItem的部分稍微修改一下,也就是item.name會佔4/6,item.price及index各佔1/6。

const renderItem = ({ item, index }) => {

const backgroundColor = index === selected ? "#f9c2ff" : "#00ffff";

return(

<TouchableOpacity onPress = {()=>setSelected(index)} style={[styles.item, {backgroundColor}]}>

<Text style={styles.title}>{item.name}</Text>

<Text style={styles.content}>{item.price}</Text>

<Text style={styles.content}>{index}</Text>

</TouchableOpacity>

)

};

作業

請改寫範例,新增產品類型、庫存量、安全存量等欄位,確認自己知道應該改那些地方:

  • 產品描述 / desc (String)

  • 價格 / price (int)

  • 產品類型 / category (String) (個人電腦、筆記型電腦、平板電腦、智慧型手機)

  • 庫存量 / inventory (int)

  • 安全存量 / safetyStock (int)

請嘗試著改變一下style,了解各種style的用法。

參考資料