この記事は、Next.js Advent Calendar 2019 24日目の記事です。
Next.jsでMobXを使用するには以下の問題を解決する必要があります
-
getInitialProps
からStoreへアクセスできない
- よってSSR時にAPIからのレスポンス値をStoreに格納できない
- SSR側でStoreに値を入れてもクライアント側に反映されない
- よってクライアント側で値の再取得になる
どちらも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
から渡ってくるStore
をProvider
へ渡します
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の中身
中身は以下の通りです。使いたい場合はこちらをコピペなりして好きに書き換えてもらって構いません
(本当はパッケージとして公開するつもりでしたが間に合いませんでした )
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側
-
getInitialProps
内 - Storeの生成
- Storeを
appContext
へ追加 (すべてのgetInitialProps
へ渡すため) -
initialState
としてpropsへStoreを渡す ( -
__NEXT_DATA__
というidがついたscript
タグにJSONのPropsデータが格納されレンダリングされる - インスタンス内
-
getInitialProps
から渡ってきたstateを元にStoreの生成 - AppコンポーネントにStoreを渡す
Client側
-
__NEXT_DATA__
内のJSONをPropsとして受け取る - グローバルにStoreが存在しているか確認
- なければ
__NEXT_DATA__
を元にStore生成、あればそれを使う(ページ遷移時に都度Storeが生成されるのを防ぐため)
以上が大まかな流れです。
まとめ
withMobx
を使用することで難しいSSRを考慮した処理が隠蔽されコードがすっきりしました。実はもう一段階tokenの取り回しという壁があるのですがそれはまた別記事に書こうと思います。