search
LoginSignup
3

More than 1 year has passed since last update.

posted at

updated at

Next.jsでのMobXの使用方法について

この記事は、Next.js Advent Calendar 2019 24日目の記事です。

Next.jsでMobXを使用するには以下の問題を解決する必要があります

  1. getInitialPropsからStoreへアクセスできない :no_good_tone1:
    • よってSSR時にAPIからのレスポンス値をStoreに格納できない
  2. SSR側でStoreに値を入れてもクライアント側に反映されない :no_good_tone1:
    • よってクライアント側で値の再取得になる

どちらもSSR特有の問題でReduxの場合は next-redux-wrapper なるものがあり解決できるが、MobXには良さそうなパッケージはありません。
実はnext-mobx-wrapper というのパッケージがありますがインストール数も少なくメンテされているようにも見えなかったので自作してみました。

withMobx

何を作ったかというとnext-redux-wrapper を模したMobX版のHOCラッパーのwithMobx
先に使い方を見ていきます。

使い方

例えば以下のようなStoreがあったとします

class AppStore {
  @observable myName: string = ''
  constructor(init: Partial<AppStore>) {
    Object.assign(this, init)
  }
  @action setMyName = (name: string) => {
    this.myName = name
  }
}

RootStoreは即にnewするのではなく、下のmakeStoreのように一度関数を挟みます

type Store = { app: AppStore }

const makeStore = (state: Partial<Store> = {}): Store => {
  return {
    app: new AppStore(state.ui)
  }
}

今回のHOCの仕様上上のようにする必要があるので注意です

_app.jsにwithMobx を反映

使い方はとてもシンプルで以下のように最後にmakeStoreを渡しつつ挟むだけです。
そしてPropから渡ってくるStoreProviderへ渡します

import React from 'react'
import App from 'next/app'
import { Provider } from 'mobx-react'
import { withMobx } from '~/helpers/withMobx'
import makeStore, { Store } from '~/stores'

type Props = {
  store: Store
}

class MyApp extends App<Props> {
  render() {
    const { Component, pageProps, store } = this.props

    return (
      <Provider {...store}>
        <Component {...pageProps} />
      </Provider>
    )
  }
}

export default withMobx(makeStore)(MyApp)

こうすることによってpages/getInitialPropsからStoreにアクセスができます

// pages/index.tsx

import React from 'react'
import { NextPage } from 'next'
import { NextJSContext } from '~/helpers/withMobx'

const LoginPage: NextPage<{ myName: string }> = ({ myName }) => {
  return <div>{myName}</div>
}
LoginPage.getInitialProps = async (ctx: NextJSContext) => {
  const { app } = ctx.store // <= Storeをゲット!
  const myName = await fetchMyName()
  app.setMyName(myName)
  return { myName }
}

export default LoginPage

withMobxの中身

中身は以下の通りです。使いたい場合はこちらをコピペなりして好きに書き換えてもらって構いません
(本当はパッケージとして公開するつもりでしたが間に合いませんでした :bow:

import React fom 'react'
import { AppContext } from 'next/app'
import { NextComponentType, NextPageContext } from 'next'

export const withMobx = (makeStore: MakeStore) => {
  const isServer = typeof window === 'undefined'

  const initStore = (initialState?: any) => {
    const storeKey = '__NEXT_MOBX_STORE__'
    const createStore = () => {
      return makeStore(initialState)
    }

    if (isServer) return createStore()

    if (!(storeKey in window)) {
      (window as any)[storeKey] = createStore()
    }

    return (window as any)[storeKey]
  }

  return (App: NextComponentType | any) =>
    class WrappedApp extends React.Component<WrappedAppProps> {
      public static displayName = `withMobx(${App.displayName || App.name || 'App'})`;

      public static getInitialProps = async (appContext: NextJSAppContext) => {
        const store = initStore()
        appContext.ctx.store = store
        appContext.ctx.isServer = isServer

        let initialProps = {}

        if ('getInitialProps' in App) {
          initialProps = await App.getInitialProps.call(App, appContext)
        }

        return {
          initialState: store,
          initialProps,
        }
      }

      public constructor(props: WrappedAppProps) {
        super(props)
        const { initialState } = props
        this.store = initStore(initialState)
      }

      protected store: any;

      public render() {
        const { initialProps, initialState, ...props } = this.props
        return (
          <App {...props} {...initialProps} store={this.store} />
        )
      }
    }
}

export type NextJSContext<Store = any> = NextPageContext & {
  store: Store
  isServer: boolean
}

export type NextJSAppContext = AppContext & {
  ctx: NextJSContext;
}

export type WrappedAppProps = {
  initialState: any
  initialProps: any
}

export type MakeStore = (initialState: any) => any

内部で何が行われているのか

(ここからは読み飛ばしてもらってOK)
このラッパーの中では以下の事が行われています

SSR側

  1. getInitialProps
    1. Storeの生成
    2. StoreをappContextへ追加 (すべてのgetInitialPropsへ渡すため)
    3. initialStateとしてpropsへStoreを渡す (
    4. __NEXT_DATA__ というidがついたscriptタグにJSONのPropsデータが格納されレンダリングされる
  2. インスタンス内
    1. getInitialPropsから渡ってきたstateを元にStoreの生成
    2. AppコンポーネントにStoreを渡す

Client側

  1. __NEXT_DATA__ 内のJSONをPropsとして受け取る
  2. グローバルにStoreが存在しているか確認
  3. なければ __NEXT_DATA__ を元にStore生成、あればそれを使う(ページ遷移時に都度Storeが生成されるのを防ぐため)

以上が大まかな流れです。

まとめ

withMobx を使用することで難しいSSRを考慮した処理が隠蔽されコードがすっきりしました。実はもう一段階tokenの取り回しという壁があるのですがそれはまた別記事に書こうと思います。

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
What you can do with signing up
3