774
567

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

経年劣化に耐える ReactComponent の書き方

Last updated at Posted at 2020-01-16

「経年劣化に耐えるコード」というのは、だれもが目指すものでしょう。「そもそもフロントエンドのコードは、今ある技術で最良のものを書き捨てるべき」という意見も理解できますが「備えあれば憂いなし」ということもありますので、ここにメモを残します。あくまで、私なりのベストプラクティスですのでご了承ください。

5層に別れた SFC

私はレイヤーによる技術の分離で、ReactComponent の経年劣化に備えています。ここでいうSFCとは「Stateless Functional Component」の略称ではありません。Vue.js の文脈にある「Single File Component」を指します。

// (1) import層
import React from 'react'
import styled from 'styled-components'
// (2) Types層
type ContainerProps = {...}
type Props = {...} & ContainerProps
// (3) DOM層
const Component: React.FC<Props> = props => (...)
// (4) Style層
const StyledComponent = styled(Component)`...`
// (5) Container層
const Container: React.FC<ContainerProps> = props => {
  return <StyledComponent {...props} />
}

記述順は「依存関係の上流下流」に従います。import や型定義が上流工程であることは言うまでもないので省略、重要なのは(3)〜(5)を構成するレイヤーです。

技術の分離

なぜこの区分になっているのか、なぜこの書式になっているのか、ひとつずつ解説していきます。

(3) DOM層

const Component: React.FC<Props> = props => (
  <div className={props.className}>
    <button onClick={props.handleClick}>
      {props.flag ? 'click me' : 'CLICK ME'}
    </button>
  </div>
)

JSX(TSX)は、React のためだけのものではなく、他ライブラリでも利用される技術です。そのため、React に依存する Hooks API などはここから取り除いています。return を用いない記法(props => (...))にすることで、Hooks API の介入を阻みます。この純粋な TSX にはビジネスロジックが無く、Array.mapや真偽値による出し分け程度です。「ボタンを押下された事で何が発生するのか?」という知識も存在しません。**ここは副作用のない、真に Stateless なレイヤーです。**このconst Componentだけを抜き出し(export)した場合、テストのしやすさは想像に易いでしょう。

(4) Style層

const StyledComponent = styled(Component)`
> button {...}
`

React CSS in JS のメジャーどころとして、styled-components がまず挙がると思いますが、Style層もあくまで CSS の話です。styled-components が解決している名前空間の解決は、BEM(MindBEMding)が解決したことと同じです。テンプレート文字列に記述されたCSSは、BEM にフォールバックしたり、CSS Modules に移行しても成立する記述となっています。私が styled-components のstyled.divstyled.button を敬遠している理由はここにあります。>による、children への指定漏洩防御も忘れない様にします。

BEM へのフォールバックはまれなケースかと思いますが、部分的に React を表示している様な折衷 html では、この切り分けが生きてきます。

(5) Container層

const Container: React.FC<ContainerProps> = props => {
  const [flag, setFlag] = React.useState(false)
  const handleClick = React.useCallback(() => {
    setFlag(!flag)
  }, [flag])
  return (
    <StyledComponent
      {...props}
      flag={flag}
      handleClick={handleClick}
    />
  )
}

Redux の経験がある方なら、PresentationalComponent / ContainerComponent というワードに馴染みがあるでしょう。Redux のコードベースには、Store に connect するコンポーネントとして、ContainerComponent という区分が明確にあります。これは React Hooks 全盛期のいまでも、踏襲すべきベストプラクティスであると私は考えています。ここは Stateful なレイヤーであり「依存の注入」を行う場所でもあります。

  • useState による状態管理が、Redux Store へ移行することになった
  • Storybook の為に、モックを注入する層に差し替えたい
  • テストの為に、モックを注入する層に差し替えたい

もしこのレイヤーに、useEffectを利用した fetch 処理が介入していたとしても、Storybook やテストにおいては、代替 Container を用意すれば良いわけです。

(1)〜(4)は、ここの都合による影響を受けることがありません。Hooks か? Redux か? GraphQL か? という配慮も当然不要なものとなります。(3) DOM層から知識を剥奪することが重要な理由はこれに起因します。

この様に「賢いレイヤー」を分離することで生まれるメリットは、依存注入技術の差し替えだけではなく、ビジネスロジックの移行(純関数の切り出し・Hooks から Redux への状態移譲)も容易にします。「Hooks API が過去のものになる…」という杞憂は当分先の話かと思いますが、将来の変化への備えとしては十分でしょう。

774
567
13

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
774
567

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?