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
Global Styling in React Native
Method 1: Custom Components
Method 2: Global Stylesheet
Method 3: Combine Both Approaches
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" (詳參: Style 、Height and Width、Layout with Flexbox 、Color Reference )。
A Comparison of Three Methods for Styling Components in React Native
Stylesheets and the Style Prop
Styled-Components
Styled-System
Styling Methods
StyleSheet
CSS-In-JS / styled-components
在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>
)
};
Flexbox (這幾篇文章都是適用於網頁,所以,命名方式不一樣)
各種style props
可以利用Yoga深入了解flexbox
可以操作並直接看效果,也可以利用GetCode取得react native的程式碼 (class component)
作業
請改寫範例,新增產品類型、庫存量、安全存量等欄位,確認自己知道應該改那些地方:
產品描述 / desc (String)
價格 / price (int)
產品類型 / category (String) (個人電腦、筆記型電腦、平板電腦、智慧型手機)
庫存量 / inventory (int)
安全存量 / safetyStock (int)
請嘗試著改變一下style,了解各種style的用法。
參考資料
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