「経年劣化に耐えるコード」というのは、だれもが目指すものでしょう。「そもそもフロントエンドのコードは、今ある技術で最良のものを書き捨てるべき」という意見も理解できますが「備えあれば憂いなし」ということもありますので、ここにメモを残します。あくまで、私なりのベストプラクティスですのでご了承ください。
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} />
}
外部 Component から見た時、隠蔽してしかるべき要素を隠蔽し、コンテキストを限定的にします。コンテキストが限定的であるほど、理解・メンテナンスしやすいコードとなります。一つのファイルに、Component の責務を閉じることは、珍しい事ではありません。
記述の順番は「依存関係の上流下流」で上から順に整理されています。import や型定義が上流工程であることは言うまでもないと思いますので省略します。重要なのは(3)〜(5)を構成するレイヤーです。
技術の分離
なぜこの区分になっているのか、なぜこの書式になっているのか、ひとつずつ解説していきます。
(3) DOM層
const Component: React.FC<Props> = props => (
<div className={props.className}>
<button onClick={props.handleClick}>
{props.flag && '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.div
やstyled.button
を敬遠している理由はここにあります。>
による、children への指定漏洩防御も忘れない様にします。
(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 を用意すれば良いわけです。(3) DOM
から知識を剥奪することが重要というのは、ここに起因します。
この様に「賢いレイヤー」を分離することで生まれるメリットは、依存注入技術の差し替えだけではなく、ビジネスロジックの移行(純関数の切り出し)も容易にします。「Hooks API が過去のものになる…」という杞憂は当分先の話かと思いますが、将来の変化への備えとしては十分でしょう。