Gatsby(with TypeScript)にReduxを組み込む

そもそもいるのかという疑問はあるかもしれませんが、チャレンジ要素は絞りたいのでreduxつかいます。

ライブラリインストール

GatsbyでRedux使う場合も、インストールするものは同じです。

$ yarn add redux react-redux

// TS使うなら
$ yarn add -D @types/react-redux

Reducerなどを作る

定番ですが、カウンターをReduxで作っておきます。Ducksで管理するのが楽なので、src/modules/index.tsに以下のコードをまとめました。

type Actions = ReturnType<typeof add> |
  ReturnType<typeof reduce> |
  ReturnType<typeof reset>

export type State = {
  count: number;
}

/**
 * Action Types
 */
export const ADD = 'ADD' as const
export const REDUCE = 'REDUCE' as const
export const RESET = 'RESET' as const

/**
 * Actions
 */
export const add = (num: number) => ({
  type: ADD,
  payload: {
    num
  }
})
export const reduce = (num: number) => ({
  type: REDUCE,
  payload: {
    num
  }
})
export const reset = () => ({
  type: RESET
})

/**
 * Reducer
 */
const initialState: State = {
  count: 0
}
export default (state: State = initialState, action: Actions) => {
  switch(action.type) {
    case ADD: {
      return {
        count: state.count + action.payload.num
      }
    }
    case REDUCE: {
      return {
        count: state.count - action.payload.num
      }
    }
    case RESET:
      return initialState
    default:
      return state;
  }
}

Storeの作成

続いてStoreを作ります。src/store/index.tsに実装しました。

import { combineReducers, createStore } from 'redux';
import firstReducer from '../modules/index'


const reducer = combineReducers<{
  first: ReturnType<typeof testReducer>
}>({
  first: firstReducer
})

export type State = ReturnType<typeof reducer>

export default () => createStore(reducer)

Reduxを使う場合の問題点

最後にProviderにStoreを入れれば終わり・・・なのですが、Gatsbyの場合はProviderをどこに書けばいいのかという問題があります。

CRAであれば一般的にsrc/App.jsxのようなファイル必ず読み込まれますので、ここにProviderを入れておけばOKです。ですが、GatsbyやNext.jsなどの場合はページ毎に読み込まれるファイルが変わりますので、別の方法を使います。

wrapRootElementを使う

Gatsbyの場合、wrapRootElementというAPIが提供されています。これを使うことで、Reduxに限らずProvider系をアプリケーション全体に設定することができます。

wrapRootElement (Function)

Allow a plugin to wrap the root element.
This is useful to set up any Provider components that will wrap your application. For setting persistent UI elements around pages use wrapPageElement.
Note: There is an equivalent hook in Gatsby’s SSR API. It is recommended to use both APIs together. For example usage, check out Using redux.

https://www.gatsbyjs.org/docs/browser-apis/#wrapRootElement

src/wrap-with-provider.tsxの実装

browserとssrどちらでも設定するため、1ファイルに記述をまとめます。

import React from 'react'
import { Provider } from 'react-redux'
import createStore from './store/index'

const store = createStore()
export default ({element}: {element: JSX.Element | JSX.Element[]}) => <Provider store={store}>{element}</Provider>

SSR & Browser両方で読み込む

あとは読み込みするようにしてやればOKです。gatsby-ssr.jsgatsby-broweser.jsに以下のコードを追加します。

import wrapWithProvider from "./src/wrap-with-provider"
export const wrapRootElement = wrapWithProvider

Reactから実行する

あとはReact SPAよろしくconnectしてdispatchしてやればOKです。

import React from "react"
import { Link } from "gatsby"
import { connect } from 'react-redux'
import * as first from '../modules/index'
import {
  State
} from '../store/index'

const IndexPage = ({count, add, reduce, reset}: ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>) => (
  <Layout>
    <h1>Hi people {count}</h1>
    <p>Welcome to your new Gatsby site.</p>
    <p>Now go build something great.</p>
    <button onClick={() => add(1)}>add</button>
    <button onClick={() => reduce(1)}>reduce</button>
    <button onClick={() => reset()}>reset</button>
  </Layout>
)

const mapStateToProps = (state: State) => {
  return {
    count: state.first.count
  }
}
const mapDispatchToProps = (dispatch: Function) => ({
  add: (num: number) => dispatch(first.add(num)),
  reduce: (num: number) => dispatch(first.reduce(num)),
  reset: () => dispatch(first.reset()),
})

export default connect(mapStateToProps, mapDispatchToProps)(IndexPage)

おわりに

Providerの設定以外は特に普通のReact-Reduxと変わりありませんでした。

ビルドした後でも問題なくReduxが動作してくれていますので、アプリとして実装したい場合にReduxが活躍してくれそうです。

Comment