SPAではない環境でReact + Reduxを使ったときの感想

最初に

最近のWebアプリを新規で開発する場合は、SPAもしくはNextやNuxtのSSRで構築するのがベストプラクティスになりつつある。 一方、既存のWebアプリの多くは、テンプレートエンジンを使ったRailsやSpring Bootなどで構築されていることが多い。しかし、流石に今更jQueryを使って非同期処理を書いて、人力でDOMに挿入するのはやりたくない... そこで、SPAではない環境でも部分的にReactとReduxを使用したくなるのだが、ピュアなSPAではないので、React + Reduxのベストプラクティスをそのまま適応するとオーバーキル感が否めない。 なので、全てを純粋関数的に記述するReactのベストプラクティスに従いすぎるのではなく、適度に妥協して導入することになると思う。

connectは使わずに、react-reduxのhooksを使用する

SPAではコンポーネントの状態はpropsによってのみ変化するというReactのベストプラクティスに従うのが良いと思いますが、部分的にReactを使用する場合はhooksで十分だと思います。 つまり、mapStateToPropsmapDispatchToPropsを使用しないということです。

useSelector

useSelectorを使用するとstoreからstateを取り出すことができます。用途としてはmapStateToPropsの代わりに使います。

import React, { FC } from 'react';
import { useSelector } from 'react-redux';
import { TodoState } from '../reducers/todoReducer';

const todoSelector = (state: { todo: TodoState }) => state.todo;

const Home: FC = () => {
  const todo = useSelector(todoSelector);
  return (
    <div>{todo.content}</div>
  );
};

export default Home;

useSelectorstate => state.reducerの関数を渡すことでstoreの状態にアクセスできます。

useStore

useStoreを使用することで、storeにアクセスできます。 以下の例はuserという名前のreducerに紐づくstateのidを取り出している例です。

import React, { FC } from 'react';
import { useStore } from 'react-redux';

const Home: FC = () => {
  const store = useStore();
  const userId = store.getState().user.id;
  return (
     ///
 );
};

export default Home;

基本的にはuseSelectorを使用する方が、型安全に書けるので望ましいと思います。 storeのデバッグなどでたまに使用します。

useDispatch

mapDistpatchToPropsの代用として使用できます。 以下はtodoリストに追加しているイメージです。

import React, { FC } from 'react';
import { useDispatch } from 'react-redux';
import { VideoState } from '../reducers/videoReducer';
import { add } from '../actions/todoAction';

const Home: FC = () => {
  const dispatch = useDispatch();
  dispatch(add("dispatch test"));
  return (
    //
  );
};

export default Home;

Preactを使用する

Reactの軽量版である、Preactでバンドルサイズを下げる。 最近のversionではreact-reduxもhooksも問題なく使用できます。

webpack.config.js

ほとんどのケースではwebpackでビルドすると思うので、webpack.config.jsを書きます。 ディレクトリ構成的には、ルートに/frontendを作成してその中に、Reactファイルをおきます。 frontend/
┣ /actions
┣ /components
┣ /containers
┣ /reducers
┣ /pages

single pageではないので、controllerに対応したview層であるhtmlがあります。なので、それぞれのhtmlに挿入するindex.tsxを/frontend/pagesに置いて、これをbuildするようにしています。

webpack.config.js

const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
const ManifestPlugin = require('webpack-manifest-plugin')

let entries = {};
glob.sync('./frontend/pages/**/*.{tsx}').map((file) => {
  let parseFile = file.split('/');
  let name = parseFile[parseFile.length -1].split('.')[0];
  entries[name] = file;
})
module.exports = {
  entry: entries,
  output: {
    filename: "[name]-[hash].js",
 // 出力先はその都度変更する。
    path: path.join(__dirname, 'public', 'assets'),
    publicPath: "/"
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        loader: 'ts-loader',
      },
    ],
  },
  resolve: {
    modules:[path.join(__dirname, 'node_modules')],
    extensions: ['.js', '.jsx', '.ts', '.tsx']
  },
  plugins: [
    new ManifestPlugin({
      writeToFileEmit: true
    }),
  ],
};

最後に

SPAでない環境でReactを使用した際に行ったことをまとめてみました。
既存プロジェクトやSPAを使用できない際に参考になれば幸いです。