next.jsとReduxを一緒にするHOC
コンテンツ:
静的アプリのReduxをセットアップするのはかなり簡単です。すべてのページに提供される単一のReduxストアを作成する必要があります。
ただし、next.js静的サイトジェネレーターまたはサーバーサイドレンダリングが関与すると、Redux接続コンポーネントをレンダリングするためにサーバーに別のストアインスタンスが必要であるため、事態は複雑になり始めます。
さらに、ページのgetInitialPropsでは、Redux Storeへのアクセスも必要になる場合があります。
これはnext-redux-wrapper役立つ場所です。ストアインスタンスを自動的に作成し、それらがすべて同じ状態を持っていることを確認します。
さらに、 App.getInitialProps getStaticPropsの複雑なケース( pages/_appを使用する場合)などの複雑getServerSidePropsケースを適切に処理できます。
ライブラリは、next.jsライフサイクル方法に関係なく、均一なインターフェイスを提供します。 Storeを使用したいと思います。
next.jsの例https://github.com/vercel/next.js/blob/canary/examples/with-redux-thunk/store.js#l23ストアはナビゲーションで置き換えられています。 Reduxは、 storeが交換された場合、メモ化されたセレクター( recomposeからのcreateSelector )でもコンポーネントを再レンダリングします:https://codesandbox.io/s/redux-store-change-kzs8qは、変更しなかったものでさえ、すべての大きな再レンダーを引き起こすことでアプリのパフォーマンスに影響を与える可能性があります。このライブラリは、 storeが同じままであることを確認します。
npm install next-redux-wrapper react-redux --save next-redux-wrapperピア依存関係としてreact-redux必要であることに注意してください。
ライブの例:https://codesandbox.io/s/next-redux-wrapper-demo-7n2t5。
すべての例はTypeScriptで書かれています。 Plain JavaScriptを使用している場合は、タイプ宣言を省略してください。これらの例は、Redux Toolkitを使用している場合は、専用の例を参照してください。
next.jsにはいくつかのデータフェッチメカニズムがあり、このライブラリはそれらのいずれかに接続できます。しかし、最初にいくつかの一般的なコードを書く必要があります。
レデューサーにはHYDRATEアクションハンドラーが必要であることに注意してください。 HYDRATEアクションハンドラーは、既存の状態の上に水和状態を適切に調整する必要があります(もしあれば)。この動作は、このライブラリのバージョン6に追加されました。この特別なアクションについては後で説明します。
store.tsという名前のファイルを作成します:
// store.ts
import { createStore , AnyAction , Store } from 'redux' ;
import { createWrapper , Context , HYDRATE } from 'next-redux-wrapper' ;
export interface State {
tick : string ;
}
// create your reducer
const reducer = ( state : State = { tick : 'init' } , action : AnyAction ) => {
switch ( action . type ) {
case HYDRATE :
// Attention! This will overwrite client state! Real apps should use proper reconciliation.
return { ... state , ... action . payload } ;
case 'TICK' :
return { ... state , tick : action . payload } ;
default :
return state ;
}
} ;
// create a makeStore function
const makeStore = ( context : Context ) => createStore ( reducer ) ;
// export an assembled wrapper
export const wrapper = createWrapper < Store < State > > ( makeStore , { debug : true } ) ; // store.js
import { createStore } from 'redux' ;
import { createWrapper , HYDRATE } from 'next-redux-wrapper' ;
// create your reducer
const reducer = ( state = { tick : 'init' } , action ) => {
switch ( action . type ) {
case HYDRATE :
return { ... state , ... action . payload } ;
case 'TICK' :
return { ... state , tick : action . payload } ;
default :
return state ;
}
} ;
// create a makeStore function
const makeStore = context => createStore ( reducer ) ;
// export an assembled wrapper
export const wrapper = createWrapper ( makeStore , { debug : true } ) ; wrapper.useWrappedStore pages/_appを使用してすべてのページを一度にラップすることを強くお勧めします。そうしないと、潜在的なレース条件が得られCannot update component while rendering another component場合があります。
import React , { FC } from 'react' ;
import { Provider } from 'react-redux' ;
import { AppProps } from 'next/app' ;
import { wrapper } from '../components/store' ;
const MyApp : FC < AppProps > = ( { Component , ... rest } ) => {
const { store , props } = wrapper . useWrappedStore ( rest ) ;
return (
< Provider store = { store } >
< Component { ... props . pageProps } />
</ Provider >
) ;
} ; wrapper.useWrappedStoreの代わりに、クラスベースのコンポーネントで動作するレガシーHOCを使用することもできます。
class MyApp extends Appを使用する場合、 getInitialPropsを提供します。これは、ラッパーによってピックアップされるため、自動静的最適化からオプトアウトされるため、 Appを拡張しないでください:https://err.sh/next.js/opt-out-auto-static-optimization。上記の例のように、通常の機能コンポーネントをエクスポートするだけです。
import React from 'react' ;
import { wrapper } from '../components/store' ;
import { AppProps } from 'next/app' ;
class MyApp extends React . Component < AppProps > {
render ( ) {
const { Component , pageProps } = this . props ;
return < Component { ... pageProps } /> ;
}
}
export default wrapper . withRedux ( MyApp ) ; getStaticPropsまたはgetServerSidePropsがユーザーによって開かれたページが排出されるたびに発送HYDRATEれます。これは、最初のページの読み込み中および通常のページナビゲーション中に発生する可能性があります。このアクションのpayloadは、静的生成またはサーバーサイドレンダリングの瞬間にstateが含まれているため、還元剤は既存のクライアント状態と適切にマージする必要があります。
最も簡単な方法は、サーバーとクライアントの状態分離を使用することです。
別の方法は、https://github.com/benjamine/jsondiffpatchを使用してdiffを分析し、適切に適用することです。
import { HYDRATE } from 'next-redux-wrapper' ;
// create your reducer
const reducer = ( state = { tick : 'init' } , action ) => {
switch ( action . type ) {
case HYDRATE :
const stateDiff = diff ( state , action . payload ) as any ;
const wasBumpedOnClient = stateDiff ?. page ?. [ 0 ] ?. endsWith ( 'X' ) ; // or any other criteria
return {
... state ,
... action . payload ,
page : wasBumpedOnClient ? state . page : action . payload . page , // keep existing state or use hydrated
} ;
case 'TICK' :
return { ... state , tick : action . payload } ;
default :
return state ;
}
} ;または、このように(next.js repoのredux-wrapperの例から):
const reducer = ( state , action ) => {
if ( action . type === HYDRATE ) {
const nextState = {
... state , // use previous state
... action . payload , // apply delta from hydration
} ;
if ( state . count ) nextState . count = state . count ; // preserve count value on client side navigation
return nextState ;
} else {
return combinedReducer ( state , action ) ;
}
} ; createWrapper関数は、 makeStore最初の引数として受け入れます。 makeStore関数は、呼び出されるたびに新しいRedux Storeインスタンスを返す必要があります。ここではメモは必要ありません。ラッパー内で自動的に行われます。
createWrapper 、オプションで構成オブジェクトを2番目のパラメーターとして受け入れます。
debug (オプション、ブール):デバッグロギングを有効にしますserializeStateおよびdeserializeState :Redux状態をシリアル化して脱着するためのカスタム関数、カスタムシリアル化と脱代化を参照してください。 makeStoreが呼び出されると、next.jsコンテキストが付属しています。これは、 NextPageContextまたはAppContextまたはgetStaticPropsまたはgetServerSidePropsコンテキストで提供されます。
これらのコンテキストのいくつか(常にgetServerSideProps 、およびNextPageContext 、 AppContextサーバーでレンダリングされる場合があります)は、リクエストと応答に関連するプロパティを持つことができます。
req ( IncomingMessage )res ( ServerResponse )両方のmakeStoreでサーバーまたはクライアント固有のロジックを作成することは可能ですが、それらは異なる動作を持たないことを強くお勧めします。これにより、エラーやチェックサムの不一致が原因で、サーバーレンダリングの目的全体を台無しにする可能性があります。
このセクションでは、getStaticPropsのライフサイクル機能を獲得するための添付方法について説明します。
pages/pageName.tsxでページを作成しましょう:
import React from 'react' ;
import { NextPage } from 'next' ;
import { useSelector } from 'react-redux' ;
import { wrapper , State } from '../store' ;
export const getStaticProps = wrapper . getStaticProps ( store => ( { preview } ) => {
console . log ( '2. Page.getStaticProps uses the store to dispatch things' ) ;
store . dispatch ( {
type : 'TICK' ,
payload : 'was set in other page ' + preview ,
} ) ;
} ) ;
// you can also use `connect()` instead of hooks
const Page : NextPage = ( ) => {
const { tick } = useSelector < State , State > ( state => state ) ;
return < div > { tick } < / div>;
} ;
export default Page ; import React from 'react' ;
import { useSelector } from 'react-redux' ;
import { wrapper } from '../store' ;
export const getStaticProps = wrapper . getStaticProps ( store => ( { preview } ) => {
console . log ( '2. Page.getStaticProps uses the store to dispatch things' ) ;
store . dispatch ( {
type : 'TICK' ,
payload : 'was set in other page ' + preview ,
} ) ;
} ) ;
// you can also use `connect()` instead of hooks
const Page = ( ) => {
const { tick } = useSelector ( state => state ) ;
return < div > { tick } </ div > ;
} ;
export default Page ;getStaticPropsのページがユーザーによって開かれたときに、 HYDRATEアクションが発送されます。このアクションのpayloadは、静的生成の瞬間にstateが含まれ、クライアント状態がないため、既存のクライアント状態と適切にマージする必要があります。サーバーとクライアントの状態分離でこれについての詳細。
個々のページをラップすることはできますが( pages/_appを包むことはできません)、お勧めしませんが、最後の段落の使用セクションを参照してください。
このセクションでは、GetServersididPropsのライフサイクル機能に接続する方法について説明します。
pages/pageName.tsxでページを作成しましょう:
import React from 'react' ;
import { NextPage } from 'next' ;
import { connect } from 'react-redux' ;
import { wrapper , State } from '../store' ;
export const getServerSideProps = wrapper . getServerSideProps ( store => ( { req , res , ... etc } ) => {
console . log ( '2. Page.getServerSideProps uses the store to dispatch things' ) ;
store . dispatch ( { type : 'TICK' , payload : 'was set in other page' } ) ;
} ) ;
// Page itself is not connected to Redux Store, it has to render Provider to allow child components to connect to Redux Store
const Page : NextPage < State > = ( { tick } ) => < div > { tick } < / div>;
// you can also use Redux `useSelector` and other hooks instead of `connect()`
export default connect ( ( state : State ) => state ) ( Page ) ; import React from 'react' ;
import { connect } from 'react-redux' ;
import { wrapper } from '../store' ;
export const getServerSideProps = wrapper . getServerSideProps ( store => ( { req , res , ... etc } ) => {
console . log ( '2. Page.getServerSideProps uses the store to dispatch things' ) ;
store . dispatch ( { type : 'TICK' , payload : 'was set in other page' } ) ;
} ) ;
// Page itself is not connected to Redux Store, it has to render Provider to allow child components to connect to Redux Store
const Page = ( { tick } ) => < div > { tick } </ div > ;
// you can also use Redux `useSelector` and other hooks instead of `connect()`
export default connect ( state => state ) ( Page ) ;getServerSidePropsがあるページがユーザーによって開かれるたびに、 HYDRATEアクションが発送されます。このアクションのpayloadは、サーバー側のレンダリングの瞬間にstateが含まれます。クライアント状態はないため、既存のクライアント状態と適切にマージする必要があります。サーバーとクライアントの状態分離でこれについての詳細。
個々のページをラップすることはできますが( pages/_appを包むことはできません)、お勧めしませんが、最後の段落の使用セクションを参照してください。
Page.getInitialProps import React , { Component } from 'react' ;
import { NextPage } from 'next' ;
import { wrapper , State } from '../store' ;
// you can also use `connect()` instead of hooks
const Page : NextPage = ( ) => {
const { tick } = useSelector < State , State > ( state => state ) ;
return < div > { tick } < / div>;
} ;
Page . getInitialProps = wrapper . getInitialPageProps ( store => ( { pathname , req , res } ) => {
console . log ( '2. Page.getInitialProps uses the store to dispatch things' ) ;
store . dispatch ( {
type : 'TICK' ,
payload : 'was set in error page ' + pathname ,
} ) ;
} ) ;
export default Page ; import React , { Component } from 'react' ;
import { wrapper } from '../store' ;
// you can also use `connect()` instead of hooks
const Page = ( ) => {
const { tick } = useSelector ( state => state ) ;
return < div > { tick } </ div > ;
} ;
Page . getInitialProps = wrapper . getInitialPageProps ( store => ( { pathname , req , res } ) => {
console . log ( '2. Page.getInitialProps uses the store to dispatch things' ) ;
store . dispatch ( {
type : 'TICK' ,
payload : 'was set in error page ' + pathname ,
} ) ;
} ) ;
export default Page ; getInitialPropsクライアント側で呼び出された場合、 reqとres利用できない場合があることに注意してください。
ステートレス関数コンポーネントは、クラスに置き換えることもできます。
class Page extends Component {
public static getInitialProps = wrapper . getInitialPageProps ( store => ( ) => ( { ... } ) ) ;
render ( ) {
// stuff
}
}
export default Page ;個々のページをラップすることはできますが( pages/_appを包むことはできません)、お勧めしませんが、最後の段落の使用セクションを参照してください。
pages/_appからアクションを発送することもできます。ただし、このモードはnext.js 9のオート部分静的エクスポート機能と互換性がありません。以下の説明を参照してください。
ラッパーは、 _appコンポーネント( /pagesにある)に添付することもできます。他のすべてのコンポーネントはreact-reduxのconnect関数を使用できます。
// pages/_app.tsx
import React from 'react' ;
import App , { AppInitialProps } from 'next/app' ;
import { wrapper } from '../components/store' ;
import { State } from '../components/reducer' ;
// Since you'll be passing more stuff to Page
declare module 'next/dist/next-server/lib/utils' {
export interface NextPageContext {
store : Store < State > ;
}
}
class MyApp extends App < AppInitialProps > {
public static getInitialProps = wrapper . getInitialAppProps ( store => async context => {
store . dispatch ( { type : 'TOE' , payload : 'was set in _app' } ) ;
return {
pageProps : {
// https://nextjs.org/docs/advanced-features/custom-app#caveats
... ( await App . getInitialProps ( context ) ) . pageProps ,
// Some custom thing for all pages
pathname : ctx . pathname ,
} ,
} ;
} ) ;
public render ( ) {
const { Component , pageProps } = this . props ;
return < Component { ... pageProps } /> ;
}
}
export default wrapper . withRedux ( MyApp ) ; // pages/_app.tsx
import React from 'react' ;
import App from 'next/app' ;
import { wrapper } from '../components/store' ;
class MyApp extends App {
static getInitialProps = wrapper . getInitialAppProps ( store => async context => {
store . dispatch ( { type : 'TOE' , payload : 'was set in _app' } ) ;
return {
pageProps : {
// https://nextjs.org/docs/advanced-features/custom-app#caveats
... ( await App . getInitialProps ( context ) ) . pageProps ,
// Some custom thing for all pages
pathname : ctx . pathname ,
} ,
} ;
} ) ;
render ( ) {
const { Component , pageProps } = this . props ;
return < Component { ... pageProps } /> ;
}
}
export default wrapper . withRedux ( MyApp ) ;その後、すべてのページを単純に接続できます(例では、ページコンポーネントを考慮します):
// pages/xxx.tsx
import React from 'react' ;
import { NextPage } from 'next' ;
import { connect } from 'react-redux' ;
import { NextPageContext } from 'next' ;
import { State } from '../store' ;
const Page : NextPage < State > = ( { foo , custom } ) => (
< div >
< div > Prop from Redux { foo } </ div >
< div > Prop from getInitialProps { custom } </ div >
</ div >
) ;
// No need to wrap pages if App was wrapped
Page . getInitialProps = ( { store , pathname , query } : NextPageContext ) => {
store . dispatch ( { type : 'FOO' , payload : 'foo' } ) ; // The component can read from the store's state when rendered
return { custom : 'custom' } ; // You can pass some custom props to the component from here
} ;
export default connect ( ( state : State ) => state ) ( Page ) ; // pages/xxx.js
import React from 'react' ;
import { connect } from 'react-redux' ;
const Page = ( { foo , custom } ) => (
< div >
< div > Prop from Redux { foo } </ div >
< div > Prop from getInitialProps { custom } </ div >
</ div >
) ;
// No need to wrap pages if App was wrapped
Page . getInitialProps = ( { store , pathname , query } ) => {
store . dispatch ( { type : 'FOO' , payload : 'foo' } ) ; // The component can read from the store's state when rendered
return { custom : 'custom' } ; // You can pass some custom props to the component from here
} ;
export default connect ( state => state ) ( Page ) ; getServerSidePropsまたはPageレベルのgetStaticPropsまた、 getServerSidePropsを使用したり、ページレベルでgetStaticProps App.getInitialProps使用getStaticPropsたりすることgetServerSidePropsできます。この場合、 HYDRATEは2回発送されます。
getServerSidePropsを使用する場合、 getServerSideProps HYDRATE storeがApp.getInitialProps後に実行され、そこから状態があります。getStaticProps使用する場合、 getStaticPropsのstoreはコンパイル時に実行され、異なるコンテキストで実行され、状態を共有できないため、 App.getInitialPropsの状態はありません。最初のHYDRATEアクションは、 App.getInitialPropsの後に述べ、2番目はgetStaticProps後に状態を持ちます(時間内に実行されたとしても)。適切なマージを確保するための最も簡単な方法は、 action.payloadから初期値を削除することです。PayLoad:
const reducer = ( state : State = { app : 'init' , page : 'init' } , action : AnyAction ) => {
switch ( action . type ) {
case HYDRATE :
if ( action . payload . app === 'init' ) delete action . payload . app ;
if ( action . payload . page === 'init' ) delete action . payload . page ;
return { ... state , ... action . payload } ;
case 'APP' :
return { ... state , app : action . payload } ;
case 'PAGE' :
return { ... state , page : action . payload } ;
default :
return state ;
}
} ; const reducer = ( state = { app : 'init' , page : 'init' } , action ) => {
switch ( action . type ) {
case HYDRATE :
if ( action . payload . app === 'init' ) delete action . payload . app ;
if ( action . payload . page === 'init' ) delete action . payload . page ;
return { ... state , ... action . payload } ;
case 'APP' :
return { ... state , app : action . payload } ;
case 'PAGE' :
return { ... state , page : action . payload } ;
default :
return state ;
}
} ;ページのみがPAGEアクションとアプリのみのAPPをディスパッチすると仮定します。これにより、状態が安全になります。
サーバーとクライアントの状態分離の詳細。
next-redux-wrapper ( "The Rapper")を使用して、リクエストで次のことが起こります。
フェーズ1: getInitialProps / getStaticProps / getServerSideProps
makeStoreを使用)を作成します。そうすることで、 makeStoreへのオプションとしてRequestとResponseオブジェクトも提供します。_appのgetInitialProps関数を呼び出し、以前に作成したストアを通過します。_appのgetInitialPropsメソッドから、店の状態から返された小道具を取ります。getXXXProps関数を呼び出し、以前に作成したストアを渡します。getXXXPropsメソッドから、ストアの状態から返された小道具を取ります。フェーズ2:SSR
makeStoreを使用して新しいストアを作成しますpayloadとして前の店の状態でHYDRATE作用を派遣します_appまたはpageコンポーネントのプロパティとして渡されます。フェーズ3:クライアント
payloadとしてフェーズ1からの状態とのHYDRATE作用を派遣します_appまたはpageコンポーネントのプロパティとして渡されます。注:クライアントの状態はリクエスト全体で持続していません(つまり、フェーズ1は常に空の状態で始まります)。したがって、ページリロードでリセットされます。リクエスト間で状態を保持する場合は、Reduxの使用を検討してください。
バージョン7.0 @reduxjs/toolkitのファーストクラスサポートが追加されているため。
完全な例:https://github.com/kirill-konshin/next-redux-wrapper/blob/master/packages/demo-redux-toolkit。
import { configureStore , createSlice , ThunkAction } from '@reduxjs/toolkit' ;
import { Action } from 'redux' ;
import { createWrapper , HYDRATE } from 'next-redux-wrapper' ;
export const subjectSlice = createSlice ( {
name : 'subject' ,
initialState : { } as any ,
reducers : {
setEnt ( state , action ) {
return action . payload ;
} ,
} ,
extraReducers : {
[ HYDRATE ] : ( state , action ) => {
console . log ( 'HYDRATE' , state , action . payload ) ;
return {
... state ,
... action . payload . subject ,
} ;
} ,
} ,
} ) ;
const makeStore = ( ) =>
configureStore ( {
reducer : {
[ subjectSlice . name ] : subjectSlice . reducer ,
} ,
devTools : true ,
} ) ;
export type AppStore = ReturnType < typeof makeStore > ;
export type AppState = ReturnType < AppStore [ 'getState' ] > ;
export type AppDispatch = AppStore [ 'dispatch' ] ;
export type AppThunk < ReturnType = void > = ThunkAction < ReturnType , AppState , unknown , Action > ;
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = ( ) => useDispatch < AppDispatch > ( ) ;
export const useAppSelector : TypedUseSelectorHook < RootState > = useSelector ;
export const fetchSubject =
( id : any ) : AppThunk =>
async dispatch => {
const timeoutPromise = ( timeout : number ) => new Promise ( resolve => setTimeout ( resolve , timeout ) ) ;
await timeoutPromise ( 200 ) ;
dispatch (
subjectSlice . actions . setEnt ( {
[ id ] : {
id ,
name : `Subject ${ id } ` ,
} ,
} ) ,
) ;
} ;
export const wrapper = createWrapper < AppStore > ( makeStore ) ;
export const selectSubject = ( id : any ) => ( state : AppState ) => state ?. [ subjectSlice . name ] ?. [ id ] ;タイプされたStateとThunkActionをエクスポートすることをお勧めします:
export type AppStore = ReturnType < typeof makeStore > ;
export type AppState = ReturnType < AppStore [ 'getState' ] > ;
export type AppThunk < ReturnType = void > = ThunkAction < ReturnType , AppState , unknown , Action > ;getStaticPropsまたはgetServerSidePropsがユーザーによって開かれたページが排出されるたびに発送HYDRATEれます。このアクションのpayloadは、静的生成またはサーバーサイドレンダリングの瞬間にstateが含まれているため、還元剤は既存のクライアント状態と適切にマージする必要があります。
何も誤って上書きされていないことを確認する最も簡単で最も安定した方法は、還元剤がクライアント側とサーバー側のアクションがあなたの状態の異なる物質に適用され、決して衝突しないことを確認することです。
export interface State {
server : any ;
client : any ;
}
const reducer = ( state : State = { tick : 'init' } , action : AnyAction ) => {
switch ( action . type ) {
case HYDRATE :
return {
... state ,
server : {
... state . server ,
... action . payload . server ,
} ,
} ;
case 'SERVER_ACTION' :
return {
... state ,
server : {
... state . server ,
tick : action . payload ,
} ,
} ;
case 'CLIENT_ACTION' :
return {
... state ,
client : {
... state . client ,
tick : action . payload ,
} ,
} ;
default :
return state ;
}
} ; const reducer = ( state = { tick : 'init' } , action ) => {
switch ( action . type ) {
case HYDRATE :
return {
... state ,
server : {
... state . server ,
... action . payload . server ,
} ,
} ;
case 'SERVER_ACTION' :
return {
... state ,
server : {
... state . server ,
tick : action . payload ,
} ,
} ;
case 'CLIENT_ACTION' :
return {
... state ,
client : {
... state . client ,
tick : action . payload ,
} ,
} ;
default :
return state ;
}
} ;州の一部の(できれば小さな)部分に対して同型アプローチを好む場合は、Next-Redux-Wrapperの拡張機能であるNext-Redux-Cookie-Wrapperを使用して、サーバーレンダリングページでクライアントとサーバーの間でそれらを共有できます。この場合、選択したサブサブサブスは、クライアントの状態を認識しており( getStaticPropsでない限り)、サーバーとクライアントの状態を分離する必要はありません。
また、https://github.com/benjamine/jsondiffpatchなどのライブラリを使用して、DIFFを分析して適切に適用することもできます。
pages/_document.jsでwithRedux使用することはお勧めしません。NEXT.JSは、コンポーネントがレンダリングされるときにシーケンスを決定する信頼できる方法を提供しません。したがって、next.jsの推奨に応じてpages/_documentにデータに依存しないものを持っている方が良いでしょう。
エラーページは、他のページと同じ方法でラップすることもできます。
エラーページ( pages/_error.jsテンプレート)への移行により、 pages/_app.jsが適用されますが、常にフルページの遷移(html5プッシュステートではありません)であるため、クライアントはサーバーからの状態を使用してゼロから作成します。したがって、クライアントにストアを保持しない限り、結果として生じる以前のクライアント状態は無視されます。
https://github.com/reduxjs/redux-thunkを使用して、Asyncアクションを発送できます。
function someAsyncAction ( id ) {
return async function ( dispatch , getState ) {
return someApiCall ( id ) . then ( res => {
dispatch ( {
type : 'FOO' ,
payload : res ,
} ) ;
} ) ;
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ;また、https://github.com/pburtchaell/redux-promise-middlewareをインストールして、Asyncアクションとして約束を派遣することもできます。図書館のインストールガイドに従って、次に次を処理できます。
function someAsyncAction ( ) {
return {
type : 'FOO' ,
payload : new Promise ( resolve => resolve ( 'foo' ) ) ,
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ;Immutable.jsまたはJSONオブジェクトなどの複雑なタイプを州に保存している場合、カスタムシリアル化および脱必要性のあるハンドラーは、サーバー上のRedux状態をシリアル化し、クライアントで再び脱isizeするのに便利かもしれません。そのために、 withReduxの構成オプションとしてserializeStateとdeserializeState提供します。
その理由は、State Snapshotが単純なオブジェクトとしてサーバーからクライアントにネットワークを介して転送されるためです。
json-immutableを使用したImmutable.js状態のカスタムシリアル化の例:
const { serialize , deserialize } = require ( 'json-immutable' ) ;
createWrapper ( {
serializeState : state => serialize ( state ) ,
deserializeState : state => deserialize ( state ) ,
} ) ;Immutable.jsを使用して同じこと:
const { fromJS } = require ( 'immutable' ) ;
createWrapper ( {
serializeState : state => state . toJS ( ) ,
deserializeState : state => fromJS ( state ) ,
} ) ;[注意してください、この方法は安全ではないかもしれません - Async Sagasを正しく処理することについて多くの考えを置いてください。注意が払わなければ、人種の状態は非常に簡単に発生します。] Redux Sagaを利用するには、 makeStore機能に何らかの変更を加える必要があります。具体的には、 redux-sagaこの関数の外側ではなく、この関数内で初期化する必要があります。 (最初はこれを行い、 Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware )。それがまさにそれを達成する方法は次のとおりです。これは、ドキュメントの先頭にあるセットアップの例からわずかに変更されています。このセットアップにより、自動静的最適化:https://err.sh/next.js/opt-outo-tatic-optimizationからオプトアウトすることに注意してください。
いつものようにルートサガを作成してから、ストア作成者を実装してください。
import { createStore , applyMiddleware , Store } from 'redux' ;
import { createWrapper , Context } from 'next-redux-wrapper' ;
import createSagaMiddleware , { Task } from 'redux-saga' ;
import reducer , { State } from './reducer' ;
import rootSaga from './saga' ;
export interface SagaStore extends Store {
sagaTask ?: Task ;
}
export const makeStore = ( context : Context ) => {
// 1: Create the middleware
const sagaMiddleware = createSagaMiddleware ( ) ;
// 2: Add an extra parameter for applying middleware:
const store = createStore ( reducer , applyMiddleware ( sagaMiddleware ) ) ;
// 3: Run your sagas on server
( store as SagaStore ) . sagaTask = sagaMiddleware . run ( rootSaga ) ;
// 4: now return the store:
return store ;
} ;
export const wrapper = createWrapper < Store < State > > ( makeStore , { debug : true } ) ; import { createStore , applyMiddleware } from 'redux' ;
import { createWrapper } from 'next-redux-wrapper' ;
import createSagaMiddleware from 'redux-saga' ;
import reducer from './reducer' ;
import rootSaga from './saga' ;
export const makeStore = context => {
// 1: Create the middleware
const sagaMiddleware = createSagaMiddleware ( ) ;
// 2: Add an extra parameter for applying middleware:
const store = createStore ( reducer , applyMiddleware ( sagaMiddleware ) ) ;
// 3: Run your sagas on server
store . sagaTask = sagaMiddleware . run ( rootSaga ) ;
// 4: now return the store:
return store ;
} ;
export const wrapper = createWrapper ( makeStore , { debug : true } ) ; pages/_appを使用しますその後、 pages/_appでサガを停止し、実行がサーバー上にあるときに終了するのを待ちます。
import React from 'react' ;
import App , { AppInitialProps } from 'next/app' ;
import { END } from 'redux-saga' ;
import { SagaStore , wrapper } from '../components/store' ;
class WrappedApp extends App < AppInitialProps > {
public static getInitialProps = wrapper . getInitialAppProps ( store => async context => {
// 1. Wait for all page actions to dispatch
const pageProps = {
// https://nextjs.org/docs/advanced-features/custom-app#caveats
... ( await App . getInitialProps ( context ) ) . pageProps ,
} ;
// 2. Stop the saga if on server
if ( context . ctx . req ) {
store . dispatch ( END ) ;
await ( store as SagaStore ) . sagaTask . toPromise ( ) ;
}
// 3. Return props
return { pageProps } ;
} ) ;
public render ( ) {
const { Component , pageProps } = this . props ;
return < Component { ... pageProps } /> ;
}
}
export default wrapper . withRedux ( WrappedApp ) ; import React from 'react' ;
import App from 'next/app' ;
import { END } from 'redux-saga' ;
import { SagaStore , wrapper } from '../components/store' ;
class WrappedApp extends App {
static getInitialProps = wrapper . getInitialAppProps ( store => async context => {
// 1. Wait for all page actions to dispatch
const pageProps = {
// https://nextjs.org/docs/advanced-features/custom-app#caveats
... ( await App . getInitialProps ( context ) ) . pageProps ,
} ;
// 2. Stop the saga if on server
if ( context . ctx . req ) {
store . dispatch ( END ) ;
await store . sagaTask . toPromise ( ) ;
}
// 3. Return props
return { pageProps } ;
} ) ;
public render ( ) {
const { Component , pageProps } = this . props ;
return < Component { ... pageProps } /> ;
}
}
export default wrapper . withRedux ( WrappedApp ) ; getServerSidePropsまたはgetStaticPropsを使用しますgetServerSidePropsまたはgetStaticPropsで使用するには、各ページのハンドラーでサガをawait必要があります。
export const getServerSideProps = ReduxWrapper . getServerSideProps ( async ( { store , req , res , ... etc } ) => {
// regular stuff
store . dispatch ( ApplicationSlice . actions . updateConfiguration ( ) ) ;
// end the saga
store . dispatch ( END ) ;
await store . sagaTask . toPromise ( ) ;
} ) ; _app内のgetInitialPropsなしの使用next.jsアプリで自動プレレンダリングをオプトアウトしたくない場合は、公式のnext.jsのようなページごとにサーバーコールされたサガを「redux saga」の例で管理できます。このオプションを使用する場合は、next.jsページメソッド内のすべてのサガを待っていることを確認してください。ページのいずれかで見逃した場合、クライアントに送信される一貫性のない状態になります。そのため、 _appで自動的に安全になるのを待つことを検討しますが、明らかに主な欠点は自動静的エクスポートからオプトアウトすることです。
州のごく一部しか持続する必要がある場合、次のレドゥークッキーワラッパーは、SSRをサポートするReduxの持続に代わる簡単な代替手段かもしれません。
ボイラープレート:https://github.com/fazlulkarimweb/with-next-redux-wrapper-redux-persist
正直なところ、サーバーはすでにHTMLをある状態で送信できるため、永続的なゲートを置く必要はないと思います。そのため、すぐに表示してから、再水和物REHYDRATEアクションが起こるのを待って、永続的なストレージからの追加のデルタが表示されるのを待つ方が良いと思います。そのため、そもそもサーバーサイドレンダリングを使用します。
しかし、再水和が起こっている間に実際にUIをブロックしたい人のために、ここに解決策があります(まだハッキーです):
// lib/redux.js
import logger from 'redux-logger' ;
import { applyMiddleware , createStore } from 'redux' ;
const SET_CLIENT_STATE = 'SET_CLIENT_STATE' ;
export const reducer = ( state , { type , payload } ) => {
// Usual stuff with HYDRATE handler
if ( type === SET_CLIENT_STATE ) {
return {
... state ,
fromClient : payload ,
} ;
}
return state ;
} ;
const makeConfiguredStore = reducer => createStore ( reducer , undefined , applyMiddleware ( logger ) ) ;
const makeStore = ( ) => {
const isServer = typeof window === 'undefined' ;
if ( isServer ) {
return makeConfiguredStore ( reducer ) ;
} else {
// we need it only on client side
const { persistStore , persistReducer } = require ( 'redux-persist' ) ;
const storage = require ( 'redux-persist/lib/storage' ) . default ;
const persistConfig = {
key : 'nextjs' ,
whitelist : [ 'fromClient' ] , // make sure it does not clash with server keys
storage ,
} ;
const persistedReducer = persistReducer ( persistConfig , reducer ) ;
const store = makeConfiguredStore ( persistedReducer ) ;
store . __persistor = persistStore ( store ) ; // Nasty hack
return store ;
}
} ;
export const wrapper = createWrapper ( makeStore ) ;
export const setClientState = clientState => ( {
type : SET_CLIENT_STATE ,
payload : clientState ,
} ) ;次に、next.js _appページで、ベアコンテキストアクセスを使用してストアを取得できます(https://react-redux.js.org/api/provider#props):
// pages/_app.tsx
import React from 'react' ;
import App from 'next/app' ;
import { ReactReduxContext } from 'react-redux' ;
import { wrapper } from './lib/redux' ;
import { PersistGate } from 'redux-persist/integration/react' ;
export default wrapper . withRedux (
class MyApp extends App {
render ( ) {
const { Component , pageProps } = this . props ;
return (
< ReactReduxContext . Consumer >
{ ( { store } ) => (
< PersistGate persistor = { store . __persistor } loading = { < div > Loading </ div > } >
< Component { ... pageProps } />
</ PersistGate >
) }
</ ReactReduxContext . Consumer >
) ;
}
} ,
) ;またはフックの使用:
// pages/_app.tsx
import React from 'react' ;
import App from 'next/app' ;
import { useStore } from 'react-redux' ;
import { wrapper } from './lib/redux' ;
import { PersistGate } from 'redux-persist/integration/react' ;
export default wrapper . withRedux ( ( { Component , pageProps } ) => {
const store = useStore ( ) ;
return (
< PersistGate persistor = { store . __persistor } loading = { < div > Loading </ div > } >
< Component { ... pageProps } />
</ PersistGate >
) ;
} ) ;そして、next.jsページで:
// pages/index.js
import React from 'react' ;
import { connect } from 'react-redux' ;
export default connect ( state => state , { setClientState } ) ( ( { fromServer , fromClient , setClientState } ) => (
< div >
< div > fromServer: { fromServer } </ div >
< div > fromClient: { fromClient } </ div >
< div >
< button onClick = { e => setClientState ( 'bar' ) } > Set Client State </ button >
</ div >
</ div >
) ) ; createWrapperの署名が変更されました。CreateWrapper createWrapper<State>の代わりに、 createWrapper<Store<State>>使用する必要があります。すべてのタイプは、 Storeから自動的に推測されます。
GetServerSidePropsContextおよびGetStaticPropsContext 、 next-redux-wrapperからエクスポートされなくなり、 GetServerSideProps 、 GetServerSidePropsContext 、 GetStaticProps 、 GetStaticPropsContext nextから直接使用する必要があります。
({store, req, res, ...}) => { ... }のようなすべての署名はstore => ({req, res, ...}) => { ... }に変更されました。
バージョン7.xでは、すべてのgetInitialProps適切なラッパーで手動でラップする必要があります: wrapper.getInitialPagePropsおよびwrapper.getInitialAppProps 。
window.next_redux_wrapper_storeは、ホットリロードの問題を引き起こしているため削除されました
バージョン6に物事が包まれる方法の大きな変化。
デフォルトのエクスポートwithReduxには削除されます。ラッパーconst wrapper = createWrapper(makeStore, {debug: true})を作成し、 wrapper.withRedux(MyApp)を使用する必要があります。
makeStore関数はinitialStateを取得しなくなり、Context: makeStore(context: Context)のみを受信します。コンテキストは、 NextPageContextまたはAppContextまたはgetStaticPropsまたはgetServerSidePropsコンテキストである可能性があります。代わりに、還元剤のHYDRATE作用を処理する必要があります。このアクションのpayloadは、静的生成またはサーバーサイドレンダリングの瞬間にstateが含まれているため、還元剤は既存のクライアント状態と適切にマージする必要があります。
App子供をProviderと一緒に包むべきではなく、現在は内部で行われています。
isServer context / propsで渡されず、独自の関数または単純なチェックconst isServer = typeof window === 'undefined' !!context.reqまたは!!context.ctx.req 。
store 、ラップされたコンポーネントプロップに渡されません。
WrappedAppPropsはWrapperPropsに改名されました。
プロジェクトがnext.js 5および次のreduxラッパー1.xを使用していた場合、これらの指示は2.xにアップグレードするのに役立ちます。
next.jsとラッパーをアップグレードします
$ npm install next@6 --save-dev
$ npm install next-redux-wrapper@latest --save import withRedux from "next-redux-wrapper"; withRedux(...)(WrappedComponent)すべてのページで、Plain React Redux connect Hoc:
import { connect } from "react-redux" ;
export default connect ( ... ) ( WrappedComponent ) ;また、ラッパーオブジェクトベースの構成をsimple React Redux Configに再フォーマットする必要がある場合があります。
次の最小コードでpages/_app.jsファイルを作成します。
// pages/_app.js
import React from 'react'
import { Provider } from 'react-redux' ;
import App from 'next/app' ;
import { wrapper } from '../store' ;
class MyApp extends App {
static async getInitialProps = ( context ) => ( {
pageProps : {
// https://nextjs.org/docs/advanced-features/custom-app#caveats
... ( await App . getInitialProps ( context ) ) . pageProps ,
}
} ) ;
render ( ) {
const { Component , pageProps } = this . props ;
return (
< Component { ... pageProps } />
) ;
}
}
export default wrapper . withRedux ( MyApp ) ; next.js 6すべてのコンポーネントのアップグレード手順( props.urlの代わりにprops.routerなど)
それでおしまい。あなたのプロジェクトは以前と同じように機能するはずです。