🤖

GoエンジニアがReactにチャレンジして驚いた5つのこと

2023/12/23に公開

はじめに

こんにちは。Magic Momentでエンジニアをしている伊藤です。
いつもはMagic MomentのプロダクトであるMagic Moment Playbookの開発に携わっています。

元々はGo言語エンジニアとしてMagic Moment Playbookのバックエンド開発に参加し始めました。
ですが、今回フロントエンドエンジニアとしてフロント側の開発に参加することとなりました。

Go言語を使っていたエンジニアがReactを使い始めて驚いたこと、理解しづらかった部分などを書いていこうと思います。
これからフロントをやってみたいと思うバックエンドエンジニアの方の参考になれば幸いです。

そもそもReact.jsとは

Magic Moment PlaybookのフロントエンドはReact.jsを使って構築されていますが、そもそもReactとはなんなのでしょうか。

ReactはFacebookが開発したJavaScriptのライブラリです。
JavaScriptのライブラリというと、他にはVue.jsなどがあります。

Componentという存在

Reactで書かれたコードを見ると、以下のように<>で囲まれた要素がよく見られます。

return(
  <>
      <HOGE />
      <FUGA>
          <PIYO age={18} />
      </FUGA>
  </>
)

初めてReactのコードを読んだ時、この時点で「なんだこれは?」となりました。 HTMLとちょっとしたJavaScriptしか触ったことのなかった私にとって、Componentというものに対する理解はゼロでした。

多くの方はご存知だと思いますがこれはReactのComponentというものです。 Componentとは、再利用可能なUIの部品のことです。
Componentをあらかじめ定義しておくことで、どのページでも同じようなUIを簡単に作ることができます。 また、テストを書きやすくする、責任範囲を明確にするなどのメリットもあります。

React.render()メソッドが呼び出されると、Reactが構築した仮想DOMツリーにComponentが紐付けられます。 この仮想DOMツリーは、Reactが管理しているメモリ上のデータ構造です。
Reactは前回の仮想DOMと最新の仮想DOMを比較して、更新部分のみを取得します。 そして、取得された更新部分を実際のDOMに適用します。

この差分検出にReact Fiberという差分検索アルゴリズムが使われているようなのですが、ボリュームが大きすぎるので割愛します。 少し古い情報ですが、参考URLを記載しておくので興味がある方はぜひ見てみてください。

ファイルに書かれた謎のCSS

Reactでは以下のように、js拡張子のファイル内にCSSを記述できます。
JavaScriptとCSSが同居しているので、初めてみた時はファイルをそっと閉じました。

<Button>TestButton</Button>

const Button = styled.button`
  background: red;
  border-radius: 3px;
  border: 2px solid palevioletred;
  color: white;
  margin: 0 1em;
  padding: 0.25em 1em;
`

これはstyled-componentsというライブラリを使った書き方で、Componentとスタイルが紐づいています。
ComponentとCSSの紐付きがわかりやすく、スタイルの使い回しも比較的簡単なので「想定外のところにスタイルが当たってしまった」とか「よくわからない見た目になっているけどどこが原因かよくわからない」ということが発生しづらいと感じています。

そして、styled-componentsにはautomatic vendor prefixingという機能もついています。フロントエンドに馴染みのある方であれば説明は不要かもしれませんが、一応説明しますと、このautomatic vendor prefixingというのはブラウザ間の差異を吸収するための機能です。
automatic vendor prefixingがなければ、以下のようにブラウザごとのCSSをプレフィックス付きで書く必要があります。

.element {
  -webkit-transform: rotate(30deg);
  -ms-transform: rotate(30deg);
  transform: rotate(30deg);
}

ですが、styled-componentsを使うとこのプレフィックスを省略できるので、ブラウザ差分などを意識せずにより大切な箇所の実装に時間をかけられます。

参考:https://styled-components.com/

useStateが返してくる謎の配列

ReactではStateを非常によく使います。StateというのはHooksの1つで、状態を管理するためのものです。

const [hoge, setHoge] = useState(0);

状態を管理といわれても正直ピンときません。公式は以下のように説明しています。

state を使うと、ユーザの入力などの情報をコンポーネントに「記憶」させることができます。例えば、フォームコンポーネントは入力された文字を保持し、画像ギャラリのコンポーネントは選択された画像を保持できます。

参考:https://ja.react.dev/reference/react/hooks

ここで、コンポーネントに「記憶」というワードが出てきますが、stateはComponentに紐づいており、そのスコープはComponentに閉じています。
つまり次のようなコードがあった場合、同じComponent(Hoge)を呼び出しているにもかかわらず内部で定義されたstateはそれぞれ異なる値を保持します。

return(
  <>
    <HOGE />
    <HOGE />
  </>
)

Componentは内部に定義されたstateを持つことができます。そして、それぞれのstateはuseState()を呼び出すことで定義できます。 useState()はstateの実態と更新用の関数を返します。更新用の関数はsetHogeのようにsetが頭についています。
constと書かれているとおり、stateのhogeは更新できません。更新用の関数setHogeを使って更新できます。 stateが更新されると、stateが紐づくComponentは再レンダリングされます。これによってComponentが最新の状態に保たれます。
一般的な変数や定数と、stateの違いはここにあります。変数や定数が更新されてもComponentは再レンダリングされません。

また、stateの実態はペアの配列です。useState()の呼び出し方を見るとわかりますが、stateを定義する時には初期値くらいしか指定していません。 変数名やオブジェクト名などは特に何も指定しませんが、それでも問題なく動くのはstateが呼び出しの順序に依存しているからです。
stateの内部ではインデックスを持っており、呼び出されるたびにインデックスがインクリメントされていきます。これによって、何番目のインデックスか?という情報と、stateを紐づけています。
より詳細な内容は、以下のURLに記載されています。

参考:https://ja.react.dev/learn/state-a-components-memory

constって定数じゃないんですか?

以下のようなコードはReactでよく目にします。前者は定数aに関数を定義します。
後者はhogeに定数を、setHogeに更新用の関数を定義します。

const a = () => {
    console.log("a")
}

const [hoge, setHoge] = useState(0);

Go言語などを使っていると、constは定数を定義するというイメージがあります。
関数を入れるケースがあまり無いので違和感がありましたし、useState()の戻り値で更新用関数が返ってくることもより私を混乱させました。 「定数に定義した値は更新できないんじゃないの?」と。

実はこのsetHogeはhogeに値を再代入しているわけではなく、メモリ領域に新たなhogeを生み出して切り替えて使っています。
実質は同じ定数に見えていますが、内部のアドレスは異なっているということですね。 それによって表面上は値が更新され、内部は別の定数を生み出し切り替えて使っているということになります。

Memo化・・・?

memo化というのはざっくり言いますとキャッシュのことです。ReatでMemo化を使用するパターンとしては、以下のようなものがあります。

1. 負荷の高い計算をキャッシュする
2. 不要な再レンダリングを防ぐ
3. Hookが不要に更新されるのを防ぐ

1.については、図形の描画が頻繁に行われるような場合でなければほとんどのWebアプリでは発生しないケースだそうです。そのため、今回は2の不要な再レンダリングを防ぐについて説明します。
Reactでは、Componentが再レンダリングされるとそれに紐づく子Componentも再レンダリングされます。これは、子Component自体が更新されていなくても関係ありません。
つまり、不要な再レンダリングが発生しているということです。

const Top = ({firstName, lastName}) => {
    const hoge = getName(firstName, lastName);
    
    return(
        <Parent>
            <Child name={hoge} />
        </Parent>
    )
}

これを防ぐために、子Component側をmemo化します。

const Child = React.memo(({name}) => {
    return (
        <div>{name}</div>
    )
})

さらに、子Componentはpropsであるnameに依存しています。getName()は親ComponentのTopがレンダリングされるたびに異なるオブジェクトを返します(値は同じでも、メモリ領域が異なる)。
そのため<Child>は依存しているpropsが更新されたと判断し、React.memoを使っていても再レンダリングされてしまいます。次のようにMemo化することで、子Componentの再レンダリングを防ぐことができます。

const Top = ({firstName, lastName}) => {
    const hoge = React.useMemo(
        () => getName(firstName, lastName),
        [firstName, lastName]
    );

    return (
        <Parent>
            <Child name={hoge} />
        </Parent>
    )
}

useMemo()の第一引数はMemo化の対象、第二引数は依存配列です。依存配列に指定した値が変更された場合にのみ、Memo化の対象が再計算されます。

まとめ

この記事をまとめている最中にも知らない情報がたくさん出てきました。
フロントエンドはReactにとどまらずTypeScriptやCSS、HTML、ブラウザの仕様やレスポンシブ対応など、学ぶスコープが広いと感じています。
奥が深く難しい反面面白さもあると感じているので、JavaScriptやReact、フロントエンドに興味のある方にはぜひ触ってみていただきたいです。

Discussion