🎨

React Nativeスタイリングの手引き

2022/02/16に公開

はじめに

これは React Native のスタイリングで困った時に見る記事です。
今まで Web でばりばりやってきた人間が、React Native の開発で最初につまずくのは環境構築です。
そして、次につまずくのがこのスタイリングになります。

css-in-js なので Web と同じノリで書けるじゃん、と思ってやってみると少しずつ違うことに気付きます。
boz-sizing がない、display が none と flex しかないあたりで違和感を覚えつつも、
どの記事にもおまじないかのように当たり前のように書かれているflex: 1をあなたもふんいきで唱えます。
画面幅に対して80%の幅を確保しつつ中央配置画面下部の高さは固定で余った分を満たすような
Web では簡単にできたスタイリングができなくて頭を抱えてきます。
そして いつも使ってたwidth: 100%が効かない場面に遭遇したところで絶望を覚えます。

コンポーネントをどのように配置するかというレイアウト、ならびに要素の拡縮率の指定が Web のソレとは大きく違います。
この記事ではスタイリングの中でも、特にレイアウトに関する Tips を紹介していきます。

React Native ではデフォルトがフレックスボックス

React Native のスタイリングは何もせずともフレックスボックスで縦方向にコンポーネントが積み上げられていきます。
初めから下記 CSS が適用されていると考えていいです。

display: flex;
flexDirection: column;

このフレックスボックスの仕様も Web と完全に同じというわけではなく、独特のクセがあります。
React Native のスタイリングでは、このフレックスボックスの仕様を把握していないとかなり苦戦します。

gap について

少し話は逸れますが、フレックスボックスの gap は 2022 年 2 月時点で最新の 0.67.2 の React Native では使えません。
つまり、グリッドレイアウトのように各要素間に数値を指定しての余白をつけることはできません。
自分で頑張って gap 相当のロジックを都度実装する必要があります。

2018 年頃からずっと本家リポジトリでも gap をサポートするように issue は立っていますが、未だにサポートされていません。
ですが朗報です。
Safari が gap をサポートしたことをキッカケに、ついに React Native でもサポートする流れになり、まだドラフトですが PR も立っています。
Yoga(React Native のスタイリングエンジン)のパフォーマンス改善を行おうとしているみたいです。
この PR についてはこちらで確認することができます。

※ 2022年1月13日追記

React Native 0.71.0で正式にgapがサポートされました!!
ヤッタネ
サポートされたプロパティは下記の3つのようです。

  • gap
  • rowGap
  • columnGap

また、現在指定できるのは数値のみでmargin: 10のように指定すると、10px相当の余白がかけられるようです。
将来的には%などの単位も指定できるようにするとのことです。

詳細は、React Nativeのブログから確認できます。

flex: 1 とはなにか

React Native では必ず 1 番親のコンポーネントで flex: 1 がかけられます。
こうすることで、画面の幅と高さ目一杯に要素が引き伸ばされます。
ここで重要なのは1番親のコンポーネントでかけられた時だけこうなります。
子コンポーネントで同じように flex: 1 を使うと別の意味合いになるので注意してください。
フレックスボックスの仕様通りではあるのですが、説明もされず当たり前のように書かれているので初見だと混乱します。

const bg = (backgroundColor: ColorValue, opacity: number = 0.2) => {
  return {
    backgroundColor,
    opacity,
  };
};
export const App = () => {
  return (
    <View style={{ flex: 1 }}>
      <View style={[{ flex: 1 }, bg("green")]} />
      <View style={[{ flex: 1 }, bg("red")]} />
      <View style={[{ flex: 1 }, bg("blue")]} />
    </View>
  );
};

これをシュミレータで確認すると下記のような画面になります。

全てflex1をかけた時

先ほど画面目一杯に引き伸ばすと説明した flex: 1 ですが、子コンポーネントでは均等に引き伸ばされています。
子コンポーネントでの flex: 1 は親要素の幅と高さを基準に、同階層の他の要素の幅や高さを考慮して引き伸ばせるだけ引き伸ばします。
この例では親で幅と高さが 100%確保された上で、同階層に flex: 1 がかけられた要素が 3 つ並んでおり、それぞれが目一杯引き伸ばそうとします。
しかし、要素が 3 つあるのでそれぞれを 1/3 ずつ均等に割った幅と高さが確保されて配置がされます。

引き伸ばされるという点は同じですが、同階層の要素が確保できる幅と高さが考慮された上での結果となります。
逆に、1 番親のコンポーネントで flex: 1 を使うと画面目一杯に引き伸ばされる理由は、他に同階層にいるコンポーネントがいないためということになります。
また 1 番親で flex: 1 をかけないと、高さと幅が確保されないので何も表示されなくなります(react-navigation の Stack 内などは違います)。

本当は親と子の flex: 1 の意味合いは同じなのですが、React コンポーネント は必ず 1 つのコンポーネントだけを返すという仕様があります。
そのため、React Native では 1 番親の flex: 1 と子の flex: 1 では明確に振舞いが区別されます。

親の flex: 1 は必ずかける

これで flex: 1 が何してるかは分かったとは思います。
カラクリさえわかれば、別にかけなくてもいいんじゃないかと思いますが、必ずかけてください。
高さそれぞれを画面目一杯に引き延ばした上でレイアウトを考え、スタイリングをかけるのが React Native でのお作法となります。

この前提でレイアウトを組む方があらゆるケースにおいて都合が良くなります。
そしてこの確保された領域をできるだけ崩さないようにコンポーネントを積み上げていくのが、React Native における経年劣化に強いコンポーネント設計ならびに CSS 設計になっていきます。
Web の React では領域を確保していくイメージですが、React Native では 100%確保した領域の中で分配をしていくイメージに近いです。
postion: absolute な要素など一部例外はありますが、基本的にかけておいた方が幸せになれます。

React Native のスタイリングは 2 つにわかれる

React Native の スタイリング は大きく 2 つの役割にわけて考えるのが良いでしょう。

  • 要素をどこにどのように配置するかのレイアウトを決めるための CSS
  • 要素のフォントサイズや影などを装飾するための CSS

web の React とは違い、React Native ではコンポーネントが受け付ける CSS が決まっています。
React Native が提供している View コンポーネントには color や fontSize は指定できず、Text コンポーネントでないとかけることができないようになっており、よりセマンティックになっています。
型定義も同様で、web だと React.CSSProperties に全て包含されていますが、ViewStyle, TextStyle のように型定義もそれぞれ独立してわかれています。
レイアウトに関わる CSS はFlexStyleという型定義に集約されています。
FlexStyle の中にも一部装飾のために使ってるものもありますが、特に下記プロパティをレイアウトをするための CSS として使うことが多いです。

  • flex
  • flexGrow
  • flexShrink
  • flexBasis
  • alignContent
  • alignItems
  • alignSelf
  • justifyContent
  • position
  • top
  • bottom
  • right
  • left
  • margin
  • ( padding )

スタイリングする際にも、この 2 つを明確に区別して行う方が良いでしょう。
基本的にレイアウトのための余白は margin を遣いますが、ScrollView など一部では padding を使うこともあります。

width と height でのレイアウトは極力避ける

Web では画面幅あるいはコンテンツ幅の n%を領域として確保する際に width と height で%で指定することがあります。
そして、この確保した幅に対して width: 100%なボタンを配置することもあります。
Web では定石となる上記のスタイリングは、React Native では意図した見た目にならないことが少なくはありません。
こちらは flex プロパティでのレイアウトをするように心がけましょう。
width と height はコンテンツのサイズ指定でのみ使いましょう。
コンテンツのサイズ指定は flex プロパティでも行うことができますが、この設定値は flex プロパティのものが優先されます。

どのように考えるのか

大体この流れでスタイリングを行っていけば良いと筆者は考えています。

  1. 親の View コンポーネントの flex: 1 をかける
  2. 子の View コンポーネントでレイアウトを決める
  3. 孫コンポーネントに幅と高さを持つコンポーネントを指定する

実際には孫コンポーネントは大きかったりするので、孫コンポーネントの中にレイアウトが必要なケースもあります。
その場合も同様にレイアウトを決めてから幅と高さを持つコンポーネントを指定する流れでうまくいきます。

それでは、ここからは色々なレイアウトパターンを紹介していきます。

width が n%なコンテンツ

高さは固定だけど、幅はスマホの画面幅に対して常に 80%を確保するようなボタンなどで多用するレイアウトです。
flexDirection を row にするところがキーとなります。
こうすることで width を 100%確保した領域に対して、flexGrow プロパティで width を n%を確保する相当のことが行うことができます。
flexGrow プロパティは、flexBasis でも width プロパティにおきかえても同様のことができます。

export const App = () => {
  return (
    <View style={[styles.container, bg("red")]}>
      <View style={styles.buttonLayout}>
        <View style={[styles.button, bg("blue", 1)]} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  // 画面目一杯の領域を確保し、画面中央に表示
  container: {
    flex: 1,
    justifyContent: "center",
  },
  // 横並びにし、中のコンテンツを中央配置
  buttonLayout: {
    flexDirection: "row",
    justifyContent: "center",
  },
  button: {
    flexGrow: 0.8,
    height: 60,
    borderRadius: 32,
  },
});

widthを80%とるボタン

画面から上下左右数 px を描画領域から除外する

画面の横幅に対して左右から数 px は余白として使用し、コンテンツの描画領域から外すようなレイアウトです。
コンテンツやテキストの開始位置を変更する時などに行います。
marginVertical は、marginTop と marginBottom を同時に指定する CSS プロパティで、marginHorizontal は Top と Bottom を同時に指定します。

export const App = () => {
  return (
    <View style={[s.container, bg("red")]}>
      <View />
      <View />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginVertical: 48,
    marginHorizontal: 16,
  },
});

画面から上下48、左右16を余白にしたページ

一部は高さ固定で残りを満たす

タブで UI を出し分けるような画面など、タブの高さは固定で残りを描画領域で満たすようなレイアウトです。
タブのボタンにflexGrow: 1を指定していますが、こちらは 0.5 でも同じ見た目になります。
このスタイリングは、親の container が tabLayout と contentsLayout の 2 つの子を column 表示で描画しており、何も指定しないとコンテンツ分の高さしか確保されないので、その残りを flex: 1 で引き伸ばせるだけ引き伸ばしています。

export const App = () => {
  return (
    <View style={styles.container}>
      <View style={styles.tabLayout}>
        <View style={[styles.button, bg("blue")]} />
        <View style={[styles.button, bg("green")]} />
      </View>
      <View style={[styles.contentsLayout, bg("red")]}>
        <View />
        <View />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: 80,
  },
  tabLayout: {
    flexDirection: "row",
  },
  contentsLayout: {
    flex: 1,
    marginTop: 16,
    marginHorizontal: 16,
  },
  button: {
    flexGrow: 1,
    height: 60,
  },
});

タブの高さだけ確保して残りをコンテンツ描画領域として満たす画面

中央配置をベースにして部分的に表示位置を変更する

1 番親の View で画面全体のベースとなる表示位置を指定することができます。
子要素はこの表示位置を継承しますが、alighSelf プロパティでその指定を上書きすることができます。
stretch は特殊で、幅あるいは高さを指定せずとも中のコンテンツを引き伸ばします。
これは親で確保されている領域で異なり、flexDirection の向きで引き伸ばされるのが幅なのか高さなのかも変わります。

alighSelf: stretch は使わないですむのなら、使わない方が無難です。
postion: absolute がかかっているモーダルのコンテンツなどではこのプロパティ値を使うことがあります。

export const App = () => {
  return (
    <View style={styles.container}>
      <View>
        <View style={[styles.box, bg("red")]} />
      </View>
      <Gap height={16} />
      <View style={styles.leftLayaut}>
        <View style={[styles.box, bg("red")]} />
      </View>
      <Gap height={16} />
      <View style={styles.rightLayout}>
        <View style={[styles.box, bg("red")]} />
      </View>
      <Gap height={16} />
      <View style={styles.fillLayout}>
        <View style={[styles.fillBox, bg("red")]} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: 80,
    marginHorizontal: 16,
    alignItems: "center",
  },
  leftLayaut: {
    alignSelf: "flex-start",
  },
  rightLayout: {
    alignSelf: "flex-end",
  },
  fillLayout: {
    alignSelf: "stretch",
  },
  box: {
    width: 240,
    flexBasis: 60,
  },
  fillBox: {
    flexBasis: 60,
  },
});

中央配置をベースにして部分的に表示位置を変更している画面

React Native のスタイリングはクセがあり、特に Web 出身のエンジニアがこのフレックスボックスによるレイアウトで洗礼を受けやすいです。
ハマり所が多く慣れるまでが大変ですが、逆にここを乗り切ればかなりレイアウトが楽にできるようになります。
Web に比べて取れる手段がフレックスボックス一択となるので、自由度はありつつも統一のとれたスタイリングを行うことができます。
gap がないのがかなり痛いですが、もうすぐサポートされるので待ち遠しいです・・・!!!

GitHubで編集を提案
CureApp テックブログ

Discussion