React+Reduxで書く時に気をつけていること

こんにちは!
ファンデリーデザイン・システム室の町田です。

今回は、React+Redux環境でのコーディング時に私が気を付けていることについてお話しします。


目次


はじめに

現在デザイン・システム室ではjQueryを捨て、順次”React.js”に移行している最中です。
(React.jsの状態管理フレームワークは、”Redux”を採用しています)

そのため、まだ導入フェーズということもあり、React.jsの記法に慣れていないメンバーもおり、積極的に社内勉強会でReact.jsの布教活動を行っています。
(以下は社内勉強会の資料を一部抜粋)

この3つのスライドだけだと私の伝えたい思いが全く伝わって来ないと思いますが
「Reactってなんか楽しそう!」と思ってもらえるような、学習を始めるきっかけとなる資料を心掛けてつくりました。
こちらの資料は入社時研修の資料としても利用する予定となっていますので、内容が気になる方は、是非ファンデリーへの入社、お待ちしています!

話が少し脱線しましたが、この記事では私がReact+Reduxを書いていて「困ったこと」や「気を付けていること」についてお話しします。


React+Reduxを書いていて困ること

書き方に悩む…

なぜ悩むのか?悩みのタネを洗い出してみました。(表現はやや誇張している部分があります)


悩み1:ディレクトリ構成パターンありすぎ問題

【ディレクトリ構成に関するReact公式の見解】

ディレクトリ構成はプロダクトに合わせた構成にするべきで
答えはないから自分達で自由に考えてくれてOK
書いてりゃそのうち最適な構成が見えてくるはず!

とのことです。
ただし、Reactもディレクトリ構成(ファイル構成)に関して丸投げではありません。
以下、2つの方法を提示してくれています。


方法1(React).Grouping by features or routes

アプリケーションの機能単位またはルート単位でディレクトリをグループ化する方法です

common/
├─ Avatar.js
├─ Avatar.css
├─ APIUtils.js
└─ APIUtils.test.js
feed/
├─ index.js
├─ Feed.js
├─ Feed.css
├─ FeedStory.js
├─ FeedStory.test.js
└─ FeedAPI.js
profile/
├─ index.js
├─ Profile.js
├─ ProfileHeader.js
├─ ProfileHeader.css
└─ ProfileAPI.js


方法2(React).Grouping by file type

api,component,containerなど、ファイル種別でグループ化する方法です

api/
├─ APIUtils.js
├─ APIUtils.test.js
├─ ProfileAPI.js
└─ UserAPI.js
components/
├─ Feed.js
├─ Feed.css
├─ FeedStory.js
├─ Profile.js
├─ ProfileHeader.js
└─ ProfileHeader.css
containers/
├─ Feed.js
└─ Profile.js

Reduxを導入している場合

さらにReduxを導入している場合は「action-type」、「action-creater」、「reducer」の構成も合わせて再考する必要があります

Reduxのパターンでよく見かけるのが以下の2つです


方法1(Redux).Ducksパターン(モジュール統合)

action-type、action-creator、reducerを「module(モジュール)」として1つのファイルにまとめる方法です

// modules/index.js

// Actions Types
const LOAD   = 'my-app/widgets/LOAD';

// Reducer
export default function reducer(state = {}, action = {}) {
  switch (action.type) {
    // do reducer stuff
    default: return state;
  }
}

// Action Creators
export function loadWidgets() {
  return { type: LOAD };
}

このDucksパターンを導入すると…

こうなるものが

actions/
└─ index.js
reducers/
└─ index.js
types/
└─ index.js

こうなります

modules/
└─ index.js

ファイル数が減ってシンプルで良い感じです


方法2(Redux).Re-Ducksパターン(moduleを再分割)

大規模アプリで上記のDucksパターンを採用しているとmoduleがどんどん肥大化して
管理が辛くなってくる問題に直面します。
そこで登場したのがこのRe-Ducksという考え方です。

duck/
├─ actions.js
├─ index.js
├─ operations.js
├─ reducers.js
├─ selectors.js
├─ tests.js
├─ types.js
└─ utils.js

統合されていたmoduleが分割され”operation”や”selector”等、新たな概念が加わっています。
詳しくはこちらをご参照ください。


最適な方法を考える時に気を付けること

(1)階層が増えると相対パスを使ったimportが辛くなる
(2)かといって、フラットにファイルを置きすぎると管理が辛くなる
(3)1つのファイルに統合しすぎると保守が辛くなる

その結果↓

プロダクトに合わせて“丁度良い”手法を検討する必要があるという結論になります。
結局、Reactの見解同様“プロダクトや担当者の好みによる”ということです。


☆デザイン・システム室の選択

現在進めているプロダクトでは

・Grouping by features or routes(React)
・Grouping by file type(React)
・Ducks(Redux)

をミックスさせた構成を選んで導入しています。

components/
  common/
    Avatar.js
  feed/
    index.js
    Feed.js
  profile/
    index.js
    Profile.js
containers/
  feed/
    index.js
  pfofile/
    index.js
modules/
  feed/
    index.js
  profile/
    index.js

現在はこの構成で開発を進めていますが、規模が大きくなってくるにつれて
Re-Ducksパターンの導入も検討しています。

以上がディレクトリ構成についてでした。

まだまだあります。React開発時の悩み!


悩み2:CSSの書き方ありすぎ問題

Reactでのスタイルの書き方は大きく分けて4つあります。
それぞれのメリット・デメリットから”どれを選択するのが最善なのか”を考えてみたいと思います。(やや偏見あり)


方法1.CSS-in-JS

文字通り、JavaScriptの中にCSSを入れる記法です

import React from 'react';
import { makeStyles } from '@material-ui/core/styles';

const useStyles = makeStyles({
  root: {
    backgroundColor: 'red',
    color: props => props.color,
  },
});

export default function MyComponent(props) {
  const classes = useStyles(props);
  return <div className={classes.root} />;
}

JavaScriptがCSSを生成する。
(Reactの機能ではなくライブラリなのでReactはCSS-in-JSについては基本ノータッチ)
ライブラリはたくさんあるので各種ライブラリを比較したい方はこちらをご参照ください。
Material-UI使いはこのCSS-in-JSを利用している方も多いのではないでしょうか。


メリット

・ライブラリ群が豊富
・CSSとJS間で変数の共有ができる


デメリット

・擬似要素やメディアクエリの実装が苦手
・ライブラリ群が豊富すぎて選べない
(React公式曰く、選ぶのに迷ったらclassName(後述)を使ってくださいとのこと)


方法2.CSS-Module

こちらも文字通り、CSSをモジュール化してくれるものです

// components/index.js

import React from 'react';
import styles from '~/styles/list.module.scss';

export default MyComponent = props => (
  <ul>
    props.items.map((item, index) => (
      <li className={styles.item} >リスト{ index }</li>
    ))
  </ul>
)
/* styles/list.scss */
.item {
  text-decoration: none;
}

例えば、webpackのcss-loaderを使った場合、↓のように自動変換されます

<li class="list__item___1EA88" >リスト1</li>

クラス名を一意に命名してくれるので名前の衝突を防ぐことができます


メリット

・name被りを防げる(命名規則に縛られなくなる)


デメリット

・CSSとJS間で変数の共有ができない


方法3.inline-style

JSXタグ内にスタイルを書く記法です

<div style={{ height: 10 }}>
  Hello World!
</div>

※style属性はオブジェクトを返すことに注意


メリット

・開発スピードは早い
・ファイル数を減らせる


デメリット

・React公式が非推奨としている
・パフォーマンスが低下する
・保守性が最悪


方法4.className

クラス名を使用する記法です

render() {
  return <span className="menu navigation-menu">Menu</span>
}

HTMLに慣れている人は一番学習コストが低い
※classではなくclassNameなので注意


メリット

・とっつきやすい
・デザイナーが理解しやすい(重要)


デメリット

・CSSとJS間で変数の共有ができない


☆デザイン・システム室の選択

開発当初はCSS-Moduleを使用していましたがいまは辞めて
現在は一番無難なclassNameを使用しています。
(”現時点での選択”であって、”最善な選択”ではないと思っています)
理由としては、CSS-Moduleがちょっと扱いづらかったことと
難点である命名被りを運用の工夫で、現在は避けられているためです。

CSS記法の選定に関しても、“プロダクトや担当者の好み”で変わってくるので
“これが正解”というものは無いように感じました。
デザイン・システム室におきましても、組織やプロダクトの規模に変化があった際は
他のCSS記法を選定し直す必要があると考えています。

以上がCSS記法についてでした。

まだあります。React開発時の悩み!


悩み3:hooks便利すぎ問題

ReactおよびReduxにそれぞれ便利なhookが実装されました。
(React Ver.16.8以降、Redux Ver.7.1.0以降より)
このhooksを使用することで、見通しの良いコードをより描きやすくなったと同時に、記法の選択肢がさらに増えたように感じます。

以下、それぞれの代表的なhooksの説明と具体的な使い方についてです。


Reactのhooks

1.useEffect( )

ステートレスコンポーネント内でも、レンダリングの後に副作用を伴う処理が実行できるようになります。

例えば…

【useEffect( )を使用しない場合】

import React from 'react';

class MyComponent extends React.Component { // ← クラスコンポーネントとして定義する必要があった

  constructor(props) {
    super(props);
  }

  componentDidMount() {
    document.title = 'ページタイトル';
  }

  render() {
    return(
      <div>...</div>
    )
  }
}
export default MyComponent;


【useEffect( )使用した場合】

import React from 'react';

export default const MyComponent = props => {

  React.useEffect(() => {
    document.title = 'ページタイトル';
  }, []);

  return(
    <div>...</div>
  )
}

useEffect( )はcomponentDidMount( )やcomponentDidUpdate( )に似たふるまいができる(厳密には違う)ので、いままでは状態を持たせたい時はステートフルなclassコンポーネントで作成していたところを、このuseEffect( )を使用することで、ステートレスなfunctionコンポーネントのまま、描画に影響がある(副作用)処理を行う(呼び出す)ことができるようになります。
functionとしてコンポーネントを保持していた方が見通しが良く便利なので、useEffect( )は積極的に使っていきたいと思いました。

その他のReactのhooksはこちら


Reduxのhooks

2.useSelector( )

Reduxのstore-stateを容易に取得することができるようになる

【useSelector( )を使用しない場合】

import React from 'react';
import { connect } from 'react-redux';

export const CounterComponent = ({ state }) => {
  return <div>{state.counter}</div>
}

const mapStateToProps = state => ({
  state: state
})

export default connect(
  mapStateToProps
)(CounterComponent)


【useSelector( )を使用した場合】

import React from 'react';
import { useSelector } from 'react-redux';

export const CounterComponent = () => {
  const counter = useSelector(state => state.counter)
  return <div>{counter}</div>
}

useSelectorを使用した場合の方がすっきりしてますね。


3.useDispatch( )

Reduxのdispatchが容易にできるようになる

【useDispatch( )を使用しない場合】

import React from 'react';
import { connect } from 'react-redux';

export const CounterComponent = ({ incrementCounter }) => {

  return (
    <div>
      <button onClick={() => incrementCounter()}>
        Increment counter
      </button>
    </div>
  )
}

const mapDispatchToProps = dispatch => ({
  incrementCounter:() => dispatch({ type: 'increment-counter' })
})

export default connect(
  mapDispatchToProps
)(CounterComponent)


【useDispatch( )を使用した場合】

import React from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) => {
  const dispatch = useDispatch()

  return (
    <div>
      <span>{value}</span>
      <button onClick={() => dispatch({ type: 'increment-counter' })}>
        Increment counter
      </button>
    </div>
  )
}

useDispatch( )を使用した場合もだいぶすっきりした印象です。

その他のReduxのhooksはこちら


Reduxのcontainerはいらない子なのか?

Reduxには、ReactとReduxを関連付ける関数「connect」が用意されています。
このconnectを通じて、Reduxの「mapStateToProps」でReactにstore-stateを渡したり、「mapDispatchToProps」でdispachを使用することができる仕組みになっています。

// ~/containers/modals/index.js

import { connect } from 'react-redux';
import Modal from '~/components/modals';

// store-state
const mapStateToProps = state => ({
  state: state
})

// dispatch
const mapDispatchToProps = dispatch => ({
  openModal: () => dispatch({ type: 'OPEN_MODAL' })
})

// Connect
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Modal)

Reduxのcontainerでは、主にこの「mapStateToProps」、「mapDispatchToProps」をReactにconnectさせる機能を実装しており、useSelectorやuseDispatchを使用すると「mapStateToProps」、「mapDispatchToProps」をconnectする必要が無くなるため、”containerの役割自体が無くなるのでは?”と思いました。

では、本当にReduxのhooksを使用するとcontainerはいらないのでしょうか。

…答えは、限りなく無くすことができる。でもやっぱり判断は「プロダクトや担当者の好みによる」です。

理由としては、hooksを多用しcontainerを完全に無くす方法で考えた場合、本来containerで行うべき処理がcomponentに集まってきてしまいます。
また、チームのルールとしてhooksは使用せず、データ処理と描画処理は明確にファイルを分ける構成にしておくという選択もありかなと思ったりもするためです。
あとは、やっぱりステートフルなclassコンポーネントが必要な場面も時にはあるように感じています。


つまり、hooksの使用をどの程度まで許容するかについては

結論:プロダクトや担当者の好みによる

当記事の各所に出現するこの魔法の言葉「プロダクトや担当者の好みによる」ですが
React+Redux環境での開発に関わらず言えることですが、各組織の文化や好み、組織やプロダクトの規模などを無視して最適な開発方法を検討した場合、どうしてもこの結論にいきついてしまうような気がしています。


☆デザイン・システム室の選択

“基本的にはReactおよびReduxのhooksは積極的に使用する”としています。
理由としては、hooksを使用したことによる弊害が現状発生していないことと、Reactのカスタムフックも合わせて利用することで、再利用性が格段に向上するためです。
ただし、こちらも今後開発メンバーが増えてきた段階で、ルール決めをする必要があると考えています。


番外編:やりすぎ?問題

Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>?

このエラーメッセージ
Reactでコードを書いたことがある人なら一度は見たことがあるのではないでしょうか。
Reactは複数のJSXタグを返す(returnする)ことを許容していません。
例えば以下の様な例です

class Columns extends React.Component {
  render() {
    return (
      <td>Hello</td>  // ← 複数のJSXタグを返そうとしている
      <td>World</td>  // ← 複数のJSXタグを返そうとしている
    );
  }
}

上記のコードはReactがエラーをはきます。
その問題を解決してくれるのがReact.Fragmentです。


React.Fragmentとは

JSXタグの一つです。
ただし、<React.Fragment />と記述しても<fragment>のようなDOM要素は生成されません。
React.Fragmentは他のDOM要素を包んでくれる(wrapする)役割をもつ機能です。
使い方は以下の通りです。

class Columns extends React.Component {
  render() {
    return (
      <React.Fragment>
        <td>Hello</td>
        <td>World</td>
      </React.Fragment>
    );
  }
}

これでエラーが解消されます。


React.Fragmentの省略形 <>

実は、React.Fragmentはこんな書き方も出来るのですが…

class Columns extends React.Component {
  render() {
    return (
      <>
        <td>Hello</td>
        <td>World</td>
      </>
    );
  }
}

(<>も<React.Fragment>も意味は同じです)

<> ←これの使用については賛否あると思います。
私個人の考えとしては、そのコードがどんな処理をしているのかが見て想像が出来ないものはなるべく使用を控えたいと思っています。
記述量が少なくて便利ですが、これは少しやりすぎ?かなと少し感じています。(個人的に)


まとめ

予想以上に長くなってしまいましたがまとめると…
「チームのみんなにとって優しいコードを書きたいし、書いた方が良いよね」というお話でした。
デザイン・システム室ではこれからも、プロダクトやチームにとってどんな書き方が最適なのかを都度考えて実装していきたいと考えています。
現在はReact.jsを書いているメンバーがまだ少ないので、メンバーを増やして相談しながら最適な方法を今後も選択していく予定です。


そんなファンデリーデザイン・システム室では現在、Reactに興味がある方を含めメンバーを募集しています。
一緒に新しいことにチャレンジしつつ、試行錯誤しながら開発を行ってみたい方のご応募をお待ちしています。


\ご応募はこちらのボタンを押した遷移先のエントリーフォームよりお願いします/

募集要項およびエントリーフォーム >