React Native v2 - Basic Components
前言
最近要轉換跑道學習開發 React Native,目前學習的課程是 Kadi Kraman 的 React Native v2 ,現在仍然是我學習 React Native 的第一天,這篇主要是關於 React Native 的基本 Components、Styling 等內容的筆記,因為是從 web 轉來寫 app 的原因,所以我都會從 web 的角度來與 app 做比較。
目前比較不習慣的地方大概是 React Native 在 flexbox 的 direction 和 web 不同,希望會越寫越習慣啦。
如果閱讀到這篇文章的話,有些章節會有 Exercise 的章節,建議跟著動手做,因為後續的章節都會藉由 exercise 來做延伸講解。
初學 React Native,概念上應該會有不少錯誤,如果有誤的話也麻煩不吝指正,非常感謝!另外我發現自己 Blog 的 code syntax highlight 很有問題,遇到 jsx 和 tsx 都會醜得不行,還請見諒,有空我會修正 der QQ
Basic React Native Components
過去寫 React 的程式碼大概會長得像這樣:
// App.js
import React from 'react';
const App = () => {
return <div>Hello, world!</div>;
};
export default App;
首先要 import React from 'react'(這點在 React v17 已經不用做了),然後在 Component 當中寫好 JSX 的元素,而這些 JSX 元素其實就是 HTML tag,這也是為什麼上手 JSX 相對容易的關係,因為只是從原本的 HTML 知識再加上一些 JSX 可以使用到的特性。
React Native 也非常相似,不過不太一樣的地方是不能寫 HTML 元素了,而是要使用它提供的 Component,我們來直接看看 code:
// App.js
import React from 'react';
import { View, Text } from 'react-native';
const App = () => {
return (
<View>
<Text>Hello, world!</Text>
</View>
);
};
export default App;
看起來和 React 有 87% 像,不過有注意到我們使用到了 View 和 Text 兩個 React Native 提供的 Component 了對吧。
<View>:就類似於 HTML 當中的 div,當作容器來做使用<Text>: 就類似於 HTML 當中的 p,用於放置文字。<ScrollView>:因為頁面不會預設你可以進行滾動,所以如果有需要滾動的內容,就可以使用到ScrollView。
React Native v2 的簡報提及的:
<View>- if you’re already familiar with web development, you can think of<View>as a native equivalent to<div>. It’s a container to use for styling and positioning the elements within.<ScrollView>- pages do not scroll by default. If you have lots of content to display, you can use aScrollView<Text>- the<Text>component is used for displaying, you guessed it, text! In React Native, all text you want to display must be contained in<Text>tags or you’ll have errors.
Basic Components and SafeAreaView
當我們打開 App.js 並輸入以下的內容:
// App.js
import React from 'react';
import { View, Text } from 'react-native';
const App = () => {
return (
<View>
<Text>Hello, world!</Text>
</View>
);
};
export default App;
會發現到文字整個怪怪的,比如 iPhone 11 的文字會跑到一個詭異的左上角區塊,我們作法可能是增加 padding 來做推擠,但增加 padding 對於每個環境都會適用嗎?沒有,如果你去看看 Android 的 simulator,會發現它自動幫你推了 padding,那怎麼辦呢?
<SafeAreaView> for the rescue!我們要做的只是把內容包裹在 <SafeAreaView> 裡面:
// App.js
import React from 'react';
import { View, Text, SafeAreaView } from 'react-native';
const App = () => {
return (
<SafeAreaView>
<View>
<Text>Hello, world!</Text>
</View>
</SafeAreaView>
);
};
export default App;
Styling
基礎用法
當我們在 React Native 要進行 style 的撰寫時,會使用到 React Native 提供的 StyleSheet :
const styles = StyleSheet.create({
container: {
padding: 10,
},
});
StyleSheet.create 會創建一個經過優化的 style object,其實用起來就像是在 React 裡面使用 style object 那樣,因此我們可以這樣使用:
<View style={styles.container}>
<Text>Hello, world!</Text>
</View>
我們可以注意到這寫法跟寫 Web 差不多,只是在 React Native 裡面並不會寫到 px ,課程裡面說到這是代表 density-independent pixels ,但我也不懂什麼意思,講者說可以把數字的單位看成 px,只是不用寫出來。
另外,如果要用上顏色的話,可以像是這樣寫:
const styles = StyleSheet.create({
container: {
backgroundColor: 'lavender',
padding: 10,
},
});
會發現到顏色的寫法也跟寫 Web 差不多,其他的顏色顯示方法如 hex #ffffff or rgb rgb(255,255,255) or rgba rgba(255,0,0,0.3) 也都能夠使用。
另外,styles 名稱會遵照 camelCase,而不是一般 css 熟悉的 kebab-case。
更多的 style 可以參照 React Native 官方文件:
padding、border
在開發 web 的時候,如果 padding 像是這樣:
.container {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 20px;
padding-right: 20px;
}
/* 我們會縮寫成這樣:*/
.container {
padding: 10px 20px;
}
不過在 React Native 不能這樣縮寫,取而代之的是使用 paddingVertical 和 paddingHorizontal 等屬性來處理:
const styles = StyleSheet.create({
container: {
paddingVertical: 10,
paddingHorizontal: 20,
},
});
margin 也是同理,會使用 marginVertical 和 marginVertical 。
還有,border style 在 web 開發的時候會寫成這樣:
.container {
border: 1px solid #fff;
}
但 React Native 不行,一定要寫成這樣:
const styles = StyleSheet.create({
container: {
borderWidth: 1,
borderStyle: 'solid',
borderColor: '#fff',
},
});
positioning
React Native 裡都會使用 flexbox 來進行排版,不過要特別注意和 web 不同的地方是,web 預設的 flex-direction 會是 row ,但 React Native 預設的 flexDirection 則是 column。
所以如果我們要水平(horizontal)的置中元素:
const styles = StyleSheet.create({
container: {
alignItems: 'center',
},
});
如果要垂直置中當然是使用 justifyContent: 'center' 了,另外為了確保元素佔滿了整個畫面的高度,我們也必須加上 flex: 1。
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
});
Styled without StyleSheet
我們也可以不使用到 StyleSheet 來撰寫 style,直接寫 style object 進去也是沒問題的:
<View style={{ backgroundColor: 'teal' }} />
/* 上面的 code 和下面是相同的意思 */
const componentStyle = { backgroundColor: 'teal' };
<View style={componentStyle} />;
但一般來說不推薦這樣寫,因為 StyleSheet 在背後其實幫忙做了一些優化,比如 cache。除非是有某些 dynamic 的 style 需要處理,才建議使用這種方式來寫 style。
Multiple styles in one component
假如我們有多個 style 想要套用,以下面這段 code 為例子:
import React from 'react';
import { View, Text, SafeAreaView, StyleSheet } from 'react-native';
const App = () => {
return (
<SafeAreaView>
<View style={styles.container}>
<Text>Hello World</Text>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: 'pink',
alignItems: 'center',
justifyContent: 'center',
flex: 1,
},
});
假如我們想要把 backgroundColor: 'pink' 抽取出去,變成另一個獨立的 class,這時候就可以用到 array 的方式來套用 style:
import React from 'react';
import { View, Text, SafeAreaView, StyleSheet } from 'react-native';
const App = () => {
return (
<SafeAreaView>
<View style={[styles.container, styles.pink]}>
<Text>Hello World</Text>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
pink: {
backgroundColor: 'pink',
},
container: {
alignItems: 'center',
justifyContent: 'center',
flex: 1,
},
});
和 web 一樣的地方在於,比較後面的 style 會覆蓋掉前面的 style。
Side note: Styled Components
另外,寫 styled components 也是沒問題的,我們在 web 的時候會這樣寫:
import styled from 'styled-components';
const StyledDiv = styled.div`
background-color: lavender;
`;
輪到 React Native 的時候,寫起來會有 87% 像:
import styled from 'styled-components/native';
const StyledView = styled.View`
background-color: lavender;
`;
我們還會發現到,寫起來和 web 幾乎一樣了!除了 css 的命名是使用到 snack-case 以外,連單位都會使用到 px 了,我想這樣的設計就是為了讓寫 web 的人可以更快適應 React Native 的寫法。
詳細的內容也可以參考 Styled-Components: React Native
不過這門課程主要還是使用 StyleSheet ,因為才剛踏入 React Native 的領域,我也會入境隨俗先不使用 styled-components。
Styling Exercise
這章節要練習的是在 React Native 寫 style,需要完成的畫面如下:

這章節我寫出來的 code 如下:
import React from 'react';
import { View, Text, SafeAreaView, StyleSheet } from 'react-native';
const App = () => {
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.container}>
<Text style={styles.title}>
Here are some boxes of different colors
</Text>
<Text style={[styles.text, styles.cyan]}>Cyan: #2aa198</Text>
<Text style={[styles.text, styles.blue]}>Blue: #268bd2</Text>
<Text style={[styles.text, styles.magenta]}>Magenta: #d33682</Text>
<Text style={[styles.text, styles.orange]}>Orange: #cb4b16</Text>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
cyan: {
backgroundColor: '#2aa198',
},
blue: {
backgroundColor: '#268bd2',
},
magenta: {
backgroundColor: '#d33682',
},
orange: {
backgroundColor: '#cb4b16',
},
text: {
color: 'white',
textAlign: 'center',
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 10,
marginBottom: 10,
},
title: {
marginBottom: 10,
fontWeight: 'bold',
},
container: {
paddingTop: 20,
paddingHorizontal: 20,
borderRadius: 10,
flex: 1,
},
safeArea: {
flex: 1,
flexDirection: 'row',
},
});
export default App;
比較困擾的是 Text 沒辦法用到 borderRadius,可能是哪邊有出錯,就等到下一章節來看看 Kadi Kraman 是如何寫的吧!
Styling Solution
和我的寫法不同的地方是,每個有顏色的文字 Kadi Kraman 都是用 View 去包裹起來,所以最後的寫法會像是:
import React from 'react';
import { View, Text, SafeAreaView, StyleSheet } from 'react-native';
const App = () => {
return (
<SafeAreaView>
<View style={styles.container}>
<Text style={styles.heading}>
Here are some boxes of different colours
</Text>
<View style={[styles.box, styles.cyan]}>
<Text style={styles.text}>Cyan #2aa198</Text>
</View>
<View style={[styles.box, styles.blue]}>
<Text style={styles.text}>Blue #268bd2</Text>
</View>
<View style={[styles.box, styles.magenta]}>
<Text style={styles.text}>Magenta #d33682</Text>
</View>
<View style={[styles.box, styles.orange]}>
<Text style={styles.text}>Orange #cb4b16</Text>
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
paddingTop: 50,
paddingHorizontal: 10,
},
heading: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
},
text: {
fontWeight: 'bold',
color: 'white',
},
box: {
padding: 10,
borderRadius: 3,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 10,
},
cyan: {
backgroundColor: '#2aa198',
},
blue: {
backgroundColor: '#268bd2',
},
magenta: {
backgroundColor: '#d33682',
},
orange: {
backgroundColor: '#cb4b16',
},
});
這樣就可以解決我剛剛沒辦法套用到 border-radius 的問題惹。
Components
從上一章節的 code 可以發現到我們可以把每個 box 切出去變成一個 ColorBox component,事不宜遲,馬上就來動手!首先寫出我們的 ColorBox,因為要顯示「文字的顏色」以及接收他的「hex 代碼」,所以我們預計要接受兩個 props,分別是 colorName 及 hexColor:
// ColorBox.tsx
const ColorBox = ({ colorName, hexColor }) => {
return (
<View>
<Text>
{colorName}: {hexColor}
</Text>
</View>
);
};
因為寫 TypeScript 的關係,我們接著定義一下 props 的 type:
// ColorBox.tsx
/* 新增 type */
type TProps = {
colorName: string;
hexColor: string;
};
const ColorBox = ({ colorName, hexColor }: TProps) => {
return (
<View>
<Text>
{colorName}: {hexColor}
</Text>
</View>
);
};
接著我們把剛剛在 App.tsx 寫的 style 搬過來:
type TProps = {
colorName: string;
hexColor: string;
};
/* 套用 style */
const ColorBox = ({ colorName, hexColor }: TProps) => {
return (
<View style={[styles.box]}>
<Text style={styles.text}>
{colorName}: {hexColor}
</Text>
</View>
);
};
/* 把 style 搬過來 */
const styles = StyleSheet.create({
box: {
borderRadius: 3,
padding: 10,
marginBottom: 10,
alignItems: 'center',
},
text: {
color: 'white',
fontWeight: 'bold',
},
});
這時候我們還缺一個東西,就是隨著外面傳進來的 props 來決定 backgroundColor 的功能,因此我們另外在 ColorBox 裡面宣告一個 style object:
type TProps = {
colorName: string;
hexColor: string;
};
/* 宣告 style object */
const ColorBox = ({ colorName, hexColor }: TProps) => {
const colorStyle = {
backgroundColor: hexColor,
};
return (
<View style={[styles.box, colorStyle]}>
<Text style={styles.text}>
{colorName}: {hexColor}
</Text>
</View>
);
};
const styles = StyleSheet.create({
box: {
borderRadius: 3,
padding: 10,
marginBottom: 10,
alignItems: 'center',
},
text: {
color: 'white',
fontWeight: 'bold',
},
});
這樣就大功告成了!在 App.tsx 重構之後就會變成這樣:
import React from 'react';
import { View, Text, SafeAreaView, StyleSheet } from 'react-native';
import ColorBox from './components/ColorBox';
const App = () => {
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.container}>
<Text style={styles.title}>
Here are some boxes of different colors
</Text>
<ColorBox colorName="Cyan" hexColor="#2aa198" />
<ColorBox colorName="Blue" hexColor="#268bd2" />
<ColorBox colorName="Magenta" hexColor="#d33682" />
<ColorBox colorName="Orange" hexColor="#cb4b16" />
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
title: {
marginBottom: 10,
fontWeight: 'bold',
},
container: {
paddingTop: 20,
paddingHorizontal: 20,
borderRadius: 10,
flex: 1,
},
safeArea: {
flex: 1,
flexDirection: 'row',
},
});
看起來變得乾淨整齊許多了對吧!
Lists
熟悉 React 的人從上一章節的 code 應該會聯想到:「我好像重複在寫 ColorBox 欸,看起來該用個 map 了吧?」,這是我們在寫 web 時常做的事情。
不過在 React Native 不要這樣做,這是出於效能的考量,因為它並不會做相關的效能優化,假設你有 1000 個 item,它就會試著 render 出這 1000 個,就算有 990 個 item 不在 user 畫面上也是如此,如果 re-render 的話也會把 1000 個 item 重新 render,可想而知這是一件可怕的事情。
因此,我們要使用 React Native 提供的 Component: FlatList 和 SectionList
FlatList
React Native Document - FlatList
FlatList 有一堆 option 的 configuration,不過最基本的 props 有三個:
- data - this is the array of data you want to map over
- renderItem - this is a function that is passed the item and its index and will return the individual item component
- keyExtractor - this is a function that gets passed an item and its index
中文的意思大概是:
- data:你想要 map 的 array
- renderItem:會是一個 function,傳進來的 data 會是這個 array,也因此可以透過資料決定要 render 什麼 component。
- keyExtractor:item 的 unique key,相當於 React 裡面的 key。
所以以上面的 Example 舉例來說,FlatList 寫起來會像是這樣:
const FOODS = [
'Apples',
'Broccoli',
'Cookies',
'Doritos',
'Eclairs'
];
const App = () => {
return (
<SafeAreaView>
<FlatList
style={{ padding: 20}}
data={FOODS}
keyExtractor={item => item}
renderItem={({ item }) => <Food name={item} />}
/>
</SafeAreaView>
);
}
比較令我困擾的地方在於 ,renderItem 的 function 接收的是 FOODS 這個 data,因此可以透過解構的方式得到裡面的值;但 keyExtractor function 傳進來的又會是一個一個的 item,有點不太統一。
SectionList
React Native Document - SectionList
SectionList is a similar to FlatList, but it allows you to render items in sections with a header item between. The data for the SectionList is still an array, but each array item will need to be an object with a title (a string) and a data (an array) prop.
SectionList 和 FlatList 頗為相似,不太一樣的地方是它可以 render header,另外就是 SectionList 接受的 data 也不一樣,一定要接受放著 object 的 array (object[]),並且 object 的內容必須是 {title: string, data: array} 這樣的格式。
所以用起來會像是這樣:
const FOODS = [
{ title: 'Healthy', data: ['Apples', 'Broccoli']},
{ title: 'Not so Healthy', data: ['Cookies', 'Doritos', 'Eclairs']},
];
const App = () => {
return (
<SafeAreaView>
<SectionList
sections={FOODS}
keyExtractor={item => item}
renderItem={data => <Food name={data.item} />}
renderSectionHeader={({ section }) => (
<Text style={styles.header}>{section.title}</Text>
)}
/>
</SafeAreaView>
);
}
List Props
除此之外還有很多額外可以使用的 props:
- ItemSeparatorComponent- renders a custom separator between your items. Handy if you have to e.g. render a line or even something dynamic instead of building it into the list items
- ListEmptyComponent - this is rendered when the
datais an empty array or undefined. Saves you from doing conditional rendering manually! - ListFooterComponent - renders something at the bottom of the list
- ListHeaderComponent - renders something at the top of the list
- extraData - the list only gets re-rendered if the
DATAchanges. It might happen though that what you display depends on some external factors. In this case use theextraDatato pass in any variables that should also trigger a re-render when changed - horizontal - render the list horizontally instead of vertically
- numColumns - render multiple columns
- onEndReached - fires when the user has scrolled to the end of the list. Handy for pagination
Lists Exercise 📝
type Colors = {
colorName: string;
hexCode: string;
};
const COLORS = [
{ colorName: 'Base03', hexCode: '#002b36' },
{ colorName: 'Base02', hexCode: '#073642' },
{ colorName: 'Base01', hexCode: '#586e75' },
{ colorName: 'Base00', hexCode: '#657b83' },
{ colorName: 'Base0', hexCode: '#839496' },
{ colorName: 'Base1', hexCode: '#93a1a1' },
{ colorName: 'Base2', hexCode: '#eee8d5' },
{ colorName: 'Base3', hexCode: '#fdf6e3' },
{ colorName: 'Yellow', hexCode: '#b58900' },
{ colorName: 'Orange', hexCode: '#cb4b16' },
{ colorName: 'Red', hexCode: '#dc322f' },
{ colorName: 'Magenta', hexCode: '#d33682' },
{ colorName: 'Violet', hexCode: '#6c71c4' },
{ colorName: 'Blue', hexCode: '#268bd2' },
{ colorName: 'Cyan', hexCode: '#2aa198' },
{ colorName: 'Green', hexCode: '#859900' },
];
const App = () => {
const renderColorBox = ({ item }: { item: Colors }) => {
return <ColorBox colorName={item.colorName} hexColor={item.hexCode} />;
};
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.container}>
<Text style={styles.title}>
Here are some boxes of different colors
</Text>
<FlatList
data={COLORS}
renderItem={renderColorBox}
keyExtractor={(item) => item.hexCode}
/>
<ColorBox colorName="Cyan" hexColor="#2aa198" />
<ColorBox colorName="Blue" hexColor="#268bd2" />
<ColorBox colorName="Magenta" hexColor="#d33682" />
<ColorBox colorName="Orange" hexColor="#cb4b16" />
</View>
</SafeAreaView>
);
};
值得注意的是我的 renderItems 本來寫成這樣:
type Colors = {
colorName: string;
hexCode: string;
};
const renderColorBox = ({ colorName, hexCode }: Colors) => {
return <ColorBox colorName={colorName} hexColor={hexCode} />;
};
結果 renderItems 會有紅字報錯:
No overload matches this call.
Overload 1 of 2, ‘(props: FlatListProps<{ colorName: string; hexCode: string; }> | Readonly<FlatListProps<{ colorName: string; hexCode: string; }>>): FlatList<{ colorName: string; hexCode: string; }>’, gave the following error.
Type ‘({ colorName, hexCode }: Colors) => JSX.Element’ is not assignable to type ‘ListRenderItem<{ colorName: string; hexCode: string; }>’.
Types of parameters ‘__0’ and ‘info’ are incompatible.
Type ‘ListRenderItemInfo<{ colorName: string; hexCode: string; }>’ is missing the following properties from type ‘Colors’: colorName, hexCode
Overload 2 of 2, ‘(props: FlatListProps<{ colorName: string; hexCode: string; }>, context: any): FlatList<{ colorName: string; hexCode: string; }>’, gave the following error.
Type ‘({ colorName, hexCode }: Colors) => JSX.Element’ is not assignable to type ‘ListRenderItem<{ colorName: string; hexCode: string; }>’.ts(2769)
參考 TypeScript React Native Flatlist: How to give renderItem the correct type of it’s item? 之後,修改成這樣:
const renderColorBox = ({ item }: { item: Colors }) => {
return <ColorBox colorName={item.colorName} hexColor={item.hexCode} />;
};
就沒問題惹,不過因為我也沒有到很熟 TypeScript,所以還不知道為什麼不行 QQ
extra credit
For extra credit - also display the name of the color in white on the darker colors and in black on the lighter ones!
暫時無解,想說用三元運算子來決定 style object 要傳入 color: 'white 或是 color: 'black' ,但好像沒辦法。
Lists Solution
extra credit
這邊 Kadi Kraman 的解法如下:
we’ve used a little calculation to adjust text colour for the background colour. There are better algorithms to do this, but this is definitely the shortest:
parseInt(props.hexCode.replace('#', ''), 16) > 0xffffff / 1.1. Here we essentially get the lightest 10% of the background colors and display black text for these, and white for the rest.
所以我們的 ColorBox 可以改寫成這樣:
const ColorBox = ({ colorName, hexColor }: TProps) => {
const boxColor = {
backgroundColor: hexColor,
};
const textColor = {
color:
parseInt(hexColor.replace('#', ''), 16) > 0xffffff / 1.1
? 'black'
: 'white',
};
return (
<View style={[styles.box, boxColor]}>
<Text style={[styles.text, textColor]}>
{colorName}: {hexColor}
</Text>
</View>
);
};