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% 像,不過有注意到我們使用到了 ViewText 兩個 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 a ScrollView
  • <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

基礎用法

Styling

React Native 中使用 Flexbox 筆記

當我們在 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 不能這樣縮寫,取而代之的是使用 paddingVerticalpaddingHorizontal 等屬性來處理:

const styles = StyleSheet.create({
  container: {
    paddingVertical: 10,
    paddingHorizontal: 20,
  },
});

margin 也是同理,會使用 marginVerticalmarginVertical

還有,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,需要完成的畫面如下:

React Native Style Exercise

這章節我寫出來的 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

Styling Exercise 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,分別是 colorNamehexColor

// 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: FlatListSectionList

FlatList

React Native Document - FlatList

FlatList Example

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 Example

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.

SectionListFlatList 頗為相似,不太一樣的地方是它可以 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 data is 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 DATA changes. It might happen though that what you display depends on some external factors. In this case use the extraData to 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>
  );
};

References

React Native, v2