見出し画像

元Javaエンジニアが楽しく書けたReact+TypeScript+ファイル分割のススメ

はじめまして、今年の7月からスペースマーケットでフロントエンドエンジニアをしている和山と言います。
社内ではディズニーが好きな人みたいな感じで認知されている気がします!

今回何を書こうか、でも単純な入社ブログではつまらない!という思いで考えていたのですが、
「最近React書くの楽しい!じゃあその楽しくなった要因ってなんだろう」
というところを振り返ったところを書いていきたいと思います。

対象としては僕のようにサーバサイドからのスキルチェンジの方だったり、最近Reactエンジニアとして就職したけどなかなか理解できず悩んでいるみたいな方向けだと思います(※あくまで個人の感想です)。

僕自身のことについて

元Javaエンジニアと書いていますが、スペースマーケット入社前の僕は、新卒でゲームセンターの店員・店長となり、SESの会社に転職してそこからエンジニアになりました。
SESの会社は2社経験しJavaをメイン言語としたWebエンジニアをし2年半ほど経験を積みました。
エンジニアとしては現在ちょうど4年目に入ったくらいなのでまだまだひよっこです。

Javaに関して言えば経験年数とは反対にシニアエンジニアのような扱いをいく先々の案件で受けたような気がします。
そんな中で自身のフロントエンドの技術力の低さを感じ始めフロントエンドをもっと知りたいと思うようになり、フロントエンドも触れる案件をメインに知見を深めていきました。

そうこうしていたらある日スペースマーケットからスカウトメールを頂きあれよあれよという間に入社が決まり今に至ります。

スペースマーケットのReactコンポーネントの構造

とは言えフロントエンド関連の案件をいくつか経験はしましたがそこまで深い知見があるわけではありません。
「Atomic Design?ナニソレ、ウマイの?」
という感じでReact自体の書き方はある程度知っている、ただベストプラクティスは分からないみたいな状態でした。

とは言えやることはたくさんあるわけなので、フロントエンドエンジニアとして仕事をしていく必要はあります。
エンジニアとして仕事をするわけでもちろんコーディングが必要です。

かなりざっくりではありますが、弊社のコンポーネントの情報はindex.jsxに集約されておりました。
またスタイリングについてはstyled-componentsを使っております。
そのコードに倣い以下のような仕様のコンポーネントを書くとしたらこんな感じになると思います。

・テキストボックス、反映ボタン、Hello Worldと書かれた文字が表示されている
・テキストボックスには文字が入力できる
・設定ボタンを押すとWorldの部分の文字が入力された文字に切り替わる
// index.jsx
import React, { useState } from 'react'
import styled from 'styled-components'
import TextBox from 'components/atoms/TextBox'

const HelloWorld = () => {
  const [displayText, setDisplayText] = useState('World')
  const [inputValue, setInputValue] = useState('')

  // テキストボックスの入力
  const onChange = (e) => {
    setInputValue(e.target.value)
  }

  // クリック処理
  const onSubmit = (e) => {
    e.preventDefault()
    setDisplayText(inputValue)
  }
  
  return (
    <Component>
      <Form>
        <TextBox value={inputValue} onChange={onChange} />
        <SubmitButton onClick={onSubmit}>設定</SubmitButton>
      </Form>
      <Text>{`Hello ${displayText}`}</Text>
    </Component>
  )
} 

export default HelloWorld

const Component = styled.div`(省略)`
const Form = styled.form`(省略)`
const SubmitButton = styled.button`(省略)`
const Text = styled.p`(省略)`

感じた分かりづらさについて

この程度のコンポーネントなら大した大きさでもないのでこれでも全然理解できる範囲なのですが、さらに大きなコンポーネントになってくると何がなんだか分からなくなります(とは言え当時の僕は「こんな感じで書くんだ、これ分かる諸先輩方すごい・・・」くらいの認識でした)。

当時(とは言え半年前)の僕は何が理解の妨げになっていたか分からなかった状態だったと思うのですが、今考えてみると以下のような理由だったと思います。

・propsにどんな値が入ってきているか分かりづらい
・importで呼び出したコンポーネントを使っているのか、styled-componentsのコンポーネントを使っているのかぱっと見で分からない
・コンポーネントのreturn内で条件分岐していたり、returnで返却するものが大きくて分かりづらい
・return前のロジックが長くなるととても見通しが悪い

これがサーバサイドでみるとコントローラにバリデーションからDBへの問い合わせ、Viewの生成など全てやっている状態だと思います。
理解できるうちはいいのですが、改修などが入ると影響調査の範囲も広い、意図しないバグを埋め込んでしまうなどリスクが高い状況です。

コードの肥大化は、読む人が辛いのももちろんですが、書いている人もだんだん辛くなります。コードを書くのが辛い作業になってしまい、楽しくなくなってしまいます。

分かりづらさの解決

では辛いことを楽しく変えるにはどうしたらいいでしょうか。

まず僕自身がやったことは以下です。

TypeScriptの導入

そして弊社テックリード(神)が新たなコンポーネントのあり方を推進しました。

・Container/Presenter/Styled Component分割設計導入

またレビューなどの指摘から以下のことを行いました。

・カスタムフックでのロジックの切り出し

それぞれについて書きたいと思います。

TypeScriptの導入

そもそも弊社でもTypeScriptを導入しようという話がすでに上がっていて、じゃあやりましょう!というところでみなさん忙しく手が回っていなかったようです。

そんな中入社直後で手が空いていた僕がとりあえずやってみようということでこちらの作業を進めました。

導入まで紆余曲折ありましたが、なんとか1つのリポジトリで導入できました。
僕自身の導入の話ではないのですが、僕のメンターをしてくださった偉大な先輩である荒田さんがTypeScript導入の話を書いてくれておりますので是非こちらも目を通していただけると!

僕自信の感覚で言うと元々JavaをやっていたこともありTypeScript自体にはあまり拒絶反応のような物はなくむしろ歓迎です。
静的片付けの言語の方が比較的覚えておくことが少なくなるので好きです。
型があることで「あれこれは文字列だったかな?配列だったかな?」みたいに考えることがなくなりました。

Container/Presenter/Styled Component分割設計導入

個人的にはこれが1番の見通しの良さを高めた部分だと思います。
簡単に社内で説明していただいた資料の内容を記載すると・・・

・Container Component
    - ロジックに責務を負うコンポーネント
    - HTML構造、Styleに対する関心を持たない
・Presenter Component
    - HTML構造に責務を負うコンポーネント
    - ロジック、Styleに関する関心は持たない
・Styled Component
    - 見た目に責務を負うコンポーネント
    - ロジックに関する関心は極力持たない

となります。
ただこれだと分かりづらいので実際に先ほどのコードに落とし込みます。
また合わせてTypeScript化も一緒に進めていこうと思います。

// index.tsx
export { default } from './HelloWorld'
// HelloWorld.tsx  Container component
import React, { useState } from 'react'
import * as Presenter from './Presenter'
import * as Types from './type'

const HelloWorld: React.FC = () => {
  const [displayText, setDisplayText] = useState<string>('World')
  const [inputValue, setInputValue] = useState<string>('')

  // テキストボックスの入力
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(e.target.value)
  }

  // クリック処理
  const onSubmit = (e: React.MouseEvent<HTMLInputElement>) => {
    e.preventDefault()
    setDisplayText(inputValue)
  }
  
  return (
    <Presenter.HelloWorld
      formChild={
        <Form
          value={inputValue}
          onChange={onChange}
          onSubmit={onSubmit}
        />
      }
      textChild={<Text text={displayText} />}
    />
  )
}

const Form: React.FC<Types.FormProps> = (props) => {
  return <Presenter.Form {...props} />
}

const Text: React.FC<Types.TextProps> = (props) => {
  const { text } = props
  return <Presenter.Text>{text}</Presenter.Text>
}

export default HelloWorld
// Presneter.tsx Presenter Component + Styled Component
import React from 'react'
import styled from 'styled-components'
import TextBox from 'components/atoms/TextBox'

import * as Types from './type'

const StyledHelloWorld = styled.div`(省略)`
type HelloWorldProps = {
  formChild: React.ReactElement<Types.FormProps>
  textChild: React.ReactElement<Types.TextProps>
}
export const HelloWorld: React.FC<HelloWorldProps> = ({formChild, textChild}) => (
  <StyledHelloWorld>
    {formChild}
    {textChild}
  </StyledHelloWorld>
)

const StyledForm = styled.form`(省略)`
const StyledSubmitButton = styled.button`(省略)`
export const Form: React.FC<Types.FormProps> = ({inputValue, onChange, onClick}) => (
  <StyledForm>
    <TextBox value={value} onChange={onChange} />
    <StyledSubmitButton onClick={onSubmit}>設定</SubmitButton>
  </StyledForm>
)

const StyledText = styled.p`(省略)`
export const Text: React.FC = (props) => {
  <StyledText>Hello {props.children}</StyledText>
}
// type.ts
export type FormProps = {
  value: string
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
  onSubmit: (e: React.MouseEvent<HTMLInputElement>) => void
}

export type TextProps = {
  text: string
}

ちょっとコード量は増えたかもしれませんが、先ほどの課題が解消されたように見えます。

まずはContainer側から見てみます。
HelloWorldコンポーネントではvalueとtextという名称だと何の値か分かりづらいので、「inputValue」「desiplayText」といった名称をつけていましたが、FormとTextのそれぞれのコンポーネントの中では利用用途がはっきりする為受け取り側でpropsの名称を変更しています。
また、HelloWorldコンポーネントでは、並びに関する関心を持たないので、formChild、textChildとしてコンポーネントを渡すだけに留めています。
また別コンポーネントとして切り出すまでもないけどちょっと切り出したいコンポーネントが切り出しやすくなりました。
特にこういったコンポーネントが役立つパターンというのはArrayでmapを使った配列操作している場合や条件分岐でコンポーネントを出し分けたい時です。
1つのFunctional Component内でmapやifなどがreturn内に入り混じるとだんだん読み辛くなるので、ファイル内でコンポーネントを切り分けてあげるとかなり辛さが軽減されます。
最終的にexportしているのはHelloWorldコンポーネントだけなので、コンポーネントのインターフェイスが明確になったと思います。

次にPresenter + Styled Componentを見てみます。
importした物かstyled-componentなのかがぱっと見で分かりづらかったのですが、styled-componentのコンポーネントの名称にStyledをつけることで見通しがよくなりました。
また、どのコンポーネントでどんなスタインリングが使われているかも各コンポーネントの近くに記載されていることで判別がつきやすくなりました。
またこれは個人的な意見ですが、Container側の名称とPresenter側のそれぞれのコンポーネントの名称を同じにしておくと、さらに考える負担が減ると思います。
さらにPresenterファイル内ではmapやifといった出し分けに関する情報を書かないので、デザイナーの方々と協力してコンポーネントを作ると言う状況でもかなり役立つと思います。

このようにファイル分割することで見通しがよくなりました。
MVCでいうとViewが分離された状態という例えがかなりしっくりくるなと個人的には思います。
難しいと思った内容も一つ一つ細かくしていくとなんだか分かってくるのはとても楽しいです。

カスタムフックでのロジックの切り出し

とは言えこれだけではまだ辛さが残ります。
先ほどのMVCの例えでいうと「MC」+「V」な状態です。
コントローラとロジックが結合している状態なのでこれを分離する必要があります。
現在表示に関する関心は消えたので、もはやサーバサイドのリファクタリングと変わりません。
ServiceクラスやModelクラスにデータの生成などは任せ、Controllerで受け取るように変えてあげれば良いのです。

そこでカスタムフックの登場です。
同一ファイルに書いてもいいのですが、見通しを良くすることが今回の目的です。
カスタムフックはhooks.tsなどで別ファイルに切り出すことにします。

// hooks.ts
import { useState } from 'react'

export const useHelloWorld = () => {
  const [displayText, setDisplayText] = useState<string>('World')
  const [inputValue, setInputValue] = useState<string>('')

  // テキストボックスの入力
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(e.target.value)
  }

  // クリック処理
  const onSubmit = (e: React.MouseEvent<HTMLInputElement>) => {
    e.preventDefault()
    setDisplayText(inputValue)
  }
  
  const state = { displayValue, inputValue }
  const action = { onChange, onSubmit }
  return [ state, action ] as const
}
// HelloWorld.tsx  Container component
import React, { useState } from 'react'

import * as Presenter from './Presenter'
import { useHelloWorld } from './hooks'
import * as Types from './type'

const HelloWorld: React.FC = () => {
  const [state, action] = useHelloWorld()
  
  return (
    <Presenter.HelloWorld
      formChild={
        <Form
          value={state.inputValue}
          onChange={action.onChange}
          onSubmit={action.onSubmit}
        />
      }
      textChild={<Text text={state.displayText} />}
    />
  )
}

const Form: React.FC<Types.FormProps> = (props) => {
  return <Presenter.Form {...props} />
}

const Text: React.FC<Types.TextProps> = (props) => {
  const { text } = props
  return <Presenter.Text>{text}</Presenter.Text>
}

export default HelloWorld

なんということでしょう、Container Componentは状態の受け渡しに専念できるようになりました。

コンポーネント側としてはuseHelloWorldの中でどんなことをしているか知らなくてもstateやactionとして処理を扱えるようになりました。
これなら例えば、onSubmitしたら新たに処理を追加したくなってもContainerとPresenter側では何も気にする必要がなく、hooksの中で影響範囲を閉じることができます。

全体的にファイル数は増えたのですが、かなり改修コストの低いコンポーネントになったと思います。

まとめ

フロントエンドをなんだか難しいなと思っていた僕でも、それぞれの問題をファイル単位で分割してしまうこと見通しがよくなりReactを書くのが楽しくなりました。
僕の場合ちょうどテックリード(神、本当に神)が色々と提案してくれたのがとてもタイミングが良かったと思います。

また、世の中には他にも色々な設計手法などがあると思います。
今後自分でもそういったものを提案できるくらいに技術力を高めていきたいと思っています。

最後に

現在バックエンドエンジニアとして一緒に成長していける方を募集しております!

北島さんが書いたノートが本当に弊社に入社した人のリアルな感想だと思うので会社の雰囲気が気になる方は要チェックです!!!
ユニット体制によるチームのグルーヴ感と褒める文化については僕も日々感じているところです。
興味が少しでも湧いたら是非お声掛け頂ければと思います!

言い忘れていましたが、僕の家では犬を飼っています。
エンジニア界隈ではよくネコ好きも多いと聞きます(どこ情報)。
弊社掲載スペースでは実はペットOKなスペースも多く掲載されています。
そんな中でペットと一緒に行きたい!ペットの思い出の写真を撮りたい!と思ったスペースをシェアしてみようと思います。


この記事が気に入ったらサポートをしてみませんか?