Ein hoc, der als nächstes.js und Redux zusammenbringt
Inhalt:
Das Einrichten von Redux für statische Apps ist ziemlich einfach: Es muss ein einzelner Redux -Store erstellt werden, der allen Seiten bereitgestellt wird.
Wenn der statische Site-Generator oder die Renders der Server-Seite als nächstes.JS-Rendering in Anspruch genommen wird, werden jedoch auf dem Server eine weitere Store-Instanz benötigt, um redux-verbundene Komponenten zu rendern.
Darüber hinaus kann der Zugriff auf den Redux Store auch während der getInitialProps einer Seite benötigt werden.
Hier ist next-redux-wrapper nützlich: Er schafft automatisch die Store-Instanzen für Sie und stellt sicher, dass sie alle den gleichen Zustand haben.
Darüber hinaus können komplexe Fälle wie App.getInitialProps (bei Verwendung pages/_app ) zusammen mit getStaticProps oder getServerSideProps auf einzelnen Seitenebene ordnungsgemäß behandelt werden.
Die Bibliothek bietet eine einheitliche Schnittstelle, unabhängig davon, in welcher Nächsten.JS -Lebenszyklusmethode Sie den Store verwenden möchten.
In Next.js Beispiel https://github.com/vercel/next.js/blob/canary/examples/with-redux-thunk/store.js#l23 Store wird zur Navigation ersetzt. Redux wird Komponenten selbst mit memoisierten Selektoren ( createSelector aus recompose ) erneut rendern, wenn store ersetzt wird: https://codesandbox.io/s/redux-store-change-kzs8q, was die Leistung der App beeinflussen kann, indem er einen riesigen Neulehrer von allem verursacht, selbst was nicht geändert wurde. Diese Bibliothek stellt sicher, dass store gleich bleibt.
npm install next-redux-wrapper react-redux --save Beachten Sie, dass next-redux-wrapper react-redux als Peer-Abhängigkeit erfordert.
Live-Beispiel: https://codesandbox.io/s/next-redux-wrapper-demo-7n2t5.
Alle Beispiele sind in TypeScript geschrieben. Wenn Sie einfach JavaScript verwenden, lassen Sie einfach Deklarationen aus. Diese Beispiele verwenden Vanilla Redux. Wenn Sie Redux -Toolkit verwenden, finden Sie in dediziertem Beispiel.
Next.js verfügt über mehrere Datenabrufmechanismen, diese Bibliothek kann an jeden von ihnen angeschlossen werden. Aber zuerst müssen Sie einen gemeinsamen Code schreiben.
Bitte beachten Sie, dass Ihr Reduzierer den HYDRATE -Action -Handler haben muss . HYDRATE -Action -Handler muss den hydratisierten Zustand über den bestehenden Staat (falls vorhanden) ordnungsgemäß in Einklang bringen. Dieses Verhalten wurde in Version 6 dieser Bibliothek hinzugefügt. Wir werden später über diese besondere Aktion sprechen.
Erstellen Sie eine Datei mit dem Namen 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 Es wird dringend empfohlen, pages/_app zu verwenden, um alle Seiten gleichzeitig zu wickeln, ansonsten aufgrund der potenziellen Rennbedingungen, die Sie möglicherweise erhalten, können 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 >
) ;
} ; Anstelle von wrapper.useWrappedStore
getInitialProps bei der Verwendung von class MyApp extends App , die von Wrapper abgeholt wird. Sie dürfen App jedoch nicht erweitern, da Sie sich aus der automatischen statischen Optimierung entschieden haben: https://err.sh/next.js/opt-out-auto-static-optimization. Exportieren Sie einfach eine reguläre funktionale Komponente wie im obigen Beispiel.
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 ) ; Jedes Mal, wenn Seiten mit getStaticProps oder getServerSideProps vom Benutzer geöffnet werden, werden die HYDRATE versandt. Dies kann während der ersten Seitenlast und während der regulären Seitennavigation geschehen. Die payload dieser Aktion enthält den state zum Zeitpunkt der statischen Erzeugung oder der Server -Seite, sodass Ihr Reduzierer ihn mit vorhandenem Client -Status ordnungsgemäß zusammenführen muss.
Der einfachste Weg ist die Verwendung der Server- und Client -Status -Trennung.
Eine andere Möglichkeit besteht darin, https://github.com/benjamine/jsondiffpatch zu verwenden, um die Difffizierung zu analysieren und ordnungsgemäß anzuwenden:
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 ;
}
} ;Oder so wie dieses (aus dem Beispiel mit Redux-Wrapper in Next.js Repo):
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 ) ;
}
} ; Die createWrapper -Funktion akzeptiert makeStore als erstes Argument. Die makeStore -Funktion sollte jedes Mal eine neue Redux Store -Instanz zurückgeben, wenn sie aufgerufen wird. Hier wird keine Memoisierung benötigt, sie wird automatisch im Wrapper durchgeführt.
createWrapper akzeptiert optional ein Konfigurationsobjekt als zweiter Parameter:
debug (optional, boolean): Aktivieren Sie die Debug -ProtokollierungserializeState und deserializeState : Benutzerdefinierte Funktionen zur Serialisierung und Deserialisierung des Redux -Zustands finden Sie in der benutzerdefinierten Serialisierung und Deserialisierung. Wenn makeStore aufgerufen wird, wird es mit einem nächsten Kontext von.JS -Kontext versehen, der als NextPageContext oder AppContext oder getStaticProps oder getServerSideProps -Kontext sein kann, je nachdem, welche Lebenszyklusfunktion Sie einwickeln.
Einige dieser Kontexte ( getServerSideProps immer und NextPageContext , AppContext manchmal, wenn die Seite auf dem Server gerendert wird) können Anforderungs- und Antwort -zubezogene Eigenschaften haben:
req ( IncomingMessage )res ( ServerResponse ) Obwohl es möglich ist, Server- oder clientspezifische Logik in beiden makeStore zu erstellen, empfehle ich dringend, dass sie kein unterschiedliches Verhalten haben. Dies kann zu Fehlern und Überprüfungssummen führen, die wiederum den gesamten Zweck der Serverwiedergabe ruinieren.
In diesem Abschnitt wird beschrieben, wie man die Lebenszyklusfunktion von GetstaticProps befindet.
Erstellen wir eine Seite in 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 vom Benutzer geöffnet werden, werden die HYDRATE -Aktion versandt. Die payload dieser Aktion enthält den state zum Zeitpunkt der statischen Erzeugung. Sie verfügt nicht über den Kundenstaat, sodass Ihr Reduzierer ihn mit dem vorhandenen Kundenstaat ordnungsgemäß zusammenführen muss. Mehr dazu bei der Trennung von Server und Client -Status.
Obwohl Sie einzelne Seiten einwickeln können (und nicht die pages/_app ), wird es nicht empfohlen, siehe Last Absatz im Nutzungsabschnitt.
In diesem Abschnitt wird beschrieben, wie Sie an die Lebenszyklusfunktion von GetServersideProps befestigt werden können.
Erstellen wir eine Seite in 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 vom Benutzer geöffnet werden, werden die HYDRATE -Aktion versandt. Die payload dieser Aktion enthält den state zum Zeitpunkt der Server -Seite -Rendering. Sie verfügt nicht über den Client -Status, sodass Ihr Reduzierer ihn mit dem vorhandenen Client -Status ordnungsgemäß zusammenführen muss. Mehr dazu bei der Trennung von Server und Client -Status.
Obwohl Sie einzelne Seiten einwickeln können (und nicht die pages/_app ), wird es nicht empfohlen, siehe Last Absatz im Nutzungsabschnitt.
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 ; Denken Sie daran, dass req und res möglicherweise nicht verfügbar sind, wenn getInitialProps auf Kundenseite gerufen wird.
Die staatenlose Funktionskomponente kann auch durch die Klasse ersetzt werden:
class Page extends Component {
public static getInitialProps = wrapper . getInitialPageProps ( store => ( ) => ( { ... } ) ) ;
render ( ) {
// stuff
}
}
export default Page ; Obwohl Sie einzelne Seiten einwickeln können (und nicht die pages/_app ), wird es nicht empfohlen, siehe Last Absatz im Nutzungsabschnitt.
pages/_app entsenden. Dieser Modus ist jedoch nicht mit dem automatischen statischen Exportfunktion von Next.js 9 kompatibel, siehe die folgende Erläuterung.
Der Wrapper kann auch an Ihre _app -Komponente (in /pages ) angebracht werden. Alle anderen Komponenten können die connect von react-redux verwenden.
// 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 ) ;Anschließend können alle Seiten einfach verbunden werden (das Beispiel berücksichtigt Seitenkomponenten):
// 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 oder getStaticProps auf Seitenebene Sie können auch getServerSideProps oder getStaticProps auf Seitenebene verwenden. In diesem Fall wird HYDRATE -Aktion zweimal entsandt: mit Status nach App.getInitialProps und dann mit dem Status nach getServerSideProps oder getStaticProps :
getServerSideProps auf Seitenebene verwenden, werden in getServerSideProps nach App.getInitialProps store und staatlich von ihm. Daher hat das zweite HYDRATE einen vollständigen Zustand von beidengetStaticProps auf Seitenebene verwenden, wird store in getStaticProps zur Kompilierungszeit ausgeführt und hat keinen Zustand von App.getInitialProps , da sie in verschiedenen Kontexten ausgeführt werden und der Zustand nicht geteilt werden kann. Erste HYDRATE geben nach App.getInitialProps und dem zweiten Zustand nach getStaticProps (obwohl es früher ausgeführt wurde). Die einfachste Möglichkeit, um eine ordnungsgemäße Verschmelzung sicherzustellen, besteht darin, die Anfangswerte aus action.payload zu fallen. 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 ;
}
} ; Angenommen, die Seite "Seite" nur die PAGE und die App -App APP , wodurch der Zustand sich sicher macht.
Mehr dazu bei der Trennung von Server und Client -Status.
Unter Verwendung von next-redux-wrapper ("The Wrapper") passieren die folgenden Dinge auf Anfrage:
Phase 1: getInitialProps / getStaticProps / getServerSideProps
makeStore ) mit einem leeren Ausgangszustand. Dabei bietet es auch die Request und Response als Optionen für makeStore ._app auf und übergibt den zuvor getInitialProps Store.getInitialProps _app Requisiten zusammen mit dem Staat des Geschäfts zurück.getXXXProps -Funktion der Seite auf und übergibt den zuvor erstellten Store.getXXXProps -Methode der Seite zurückgegebene Requisiten zusammen mit dem Staat des Geschäfts zurück.Phase 2: SSR
makeStoreHYDRATE mit dem Status des vorherigen Geschäfts als payload_app oder page übergeben.Phase 3: Client
HYDRATE mit dem Zustand aus Phase 1 als payload_app oder page übergeben.Hinweis: Der Zustand des Kunden wird nicht über Anfragen hinweg bestehen (dh Phase 1 beginnt immer mit einem leeren Zustand). Daher wird es auf Seite Nachladen zurückgesetzt. Erwägen Sie, Redux Persist zu verwenden, wenn Sie zwischen Anfragen den Zustand bestehen möchten.
Seit Version 7.0 wurde die erstklassige Unterstützung von @reduxjs/toolkit hinzugefügt.
Vollständiges Beispiel: 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 ] ; Es wird empfohlen, typisierten State und ThunkAction zu exportieren:
export type AppStore = ReturnType < typeof makeStore > ;
export type AppState = ReturnType < AppStore [ 'getState' ] > ;
export type AppThunk < ReturnType = void > = ThunkAction < ReturnType , AppState , unknown , Action > ; Jedes Mal, wenn Seiten mit getStaticProps oder getServerSideProps vom Benutzer geöffnet werden, werden die HYDRATE versandt. Die payload dieser Aktion enthält den state zum Zeitpunkt der statischen Erzeugung oder der Server -Seite, sodass Ihr Reduzierer ihn mit vorhandenem Client -Status ordnungsgemäß zusammenführen muss.
Der einfachste und stabilste Weg, um sicherzustellen, dass nichts versehentlich überschrieben ist, besteht darin, sicherzustellen, dass Ihr Reduzierer die Aktionen der Client -Seite und Server auf verschiedene Substatrate Ihres Zustands anwendet und niemals aufeinander abgestimmt ist:
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 ;
}
} ; Wenn Sie einen isomorphen Ansatz für einige (vorzugsweise kleine) Teile Ihres Status bevorzugen, können Sie diese zwischen Client und Server auf servergerenderten Seiten mit dem nächsten Redux-Cookie-Wrapper, einer Erweiterung auf das Next-Redux-Wrapper, weitergeben. In diesem Fall ist der Server für ausgewählte Substatoren den Status des Clients bekannt (es sei denn, in getStaticProps ) und es besteht nicht erforderlich, den Server- und Client -Status zu trennen.
Außerdem können Sie eine Bibliothek wie https://github.com/benjamine/jsondiffpatch verwenden, um die Diff zu analysieren und ordnungsgemäß anzuwenden.
Ich empfehle nicht, withRedux in pages/_document.js zu verwenden. Pro NEXT.JS-Empfehlung ist es besser, nur daten-agnostische Dinge auf pages/_document zu haben.
Fehlerseiten können auch auf die gleiche Weise wie alle anderen Seiten verpackt werden.
Übergang zu einer Fehlerseite ( pages/_error.js -Vorlage) führt dazu, dass pages/_app.js angewendet werden. Es handelt sich jedoch immer um einen vollständigen Übergang (nicht html5 pushstate), sodass der Client den Speicher mit dem Status aus dem Server mit dem Status erstellt hat. Sofern Sie den Laden nicht auf dem Kunden bestehen, wird der resultierende frühere Kundenstaat ignoriert.
Sie können https://github.com/reduxjs/redux-thunk verwenden, um asynchronisierte Aktionen zu entsenden:
function someAsyncAction ( id ) {
return async function ( dispatch , getState ) {
return someApiCall ( id ) . then ( res => {
dispatch ( {
type : 'FOO' ,
payload : res ,
} ) ;
} ) ;
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ;Sie können auch https://github.com/pburtchaell/redux-promise-middleware installieren, um Versprechen als asynchronisierte Aktionen zu versenden. Befolgen Sie die Installationshandbuch der Bibliothek und Sie können sie so verarbeiten:
function someAsyncAction ( ) {
return {
type : 'FOO' ,
payload : new Promise ( resolve => resolve ( 'foo' ) ) ,
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ; Wenn Sie komplexe Typen wie unveränderliche.js- oder JSON -Objekte in Ihrem Zustand speichern, kann ein benutzerdefinierter Serialisierungs- und Deserialize -Handler praktisch sein, um den Redux -Status auf dem Server zu serialisieren und ihn erneut auf dem Client zu Deserialisierung. Geben Sie dazu serializeState und deserializeState als Konfigurationsoptionen für withRedux an.
Der Grund dafür ist, dass Status Snapshot als einfaches Objekt über das Netzwerk vom Server zum Client übertragen wird.
Beispiel für eine benutzerdefinierte Serialisierung eines json-immutable Zustands.
const { serialize , deserialize } = require ( 'json-immutable' ) ;
createWrapper ( {
serializeState : state => serialize ( state ) ,
deserializeState : state => deserialize ( state ) ,
} ) ;Gleiches gilt für unveränderliche.js:
const { fromJS } = require ( 'immutable' ) ;
createWrapper ( {
serializeState : state => state . toJS ( ) ,
deserializeState : state => fromJS ( state ) ,
} ) ; [Beachten Sie, dass diese Methode möglicherweise unsicher sein kann - stellen Sie sicher, dass Sie viel darüber nachdenken, asynchronisierte Sagen richtig zu behandeln. Rennbedingungen ereignen sich sehr leicht, wenn Sie nicht vorsichtig sind.] Um Redux -Saga zu verwenden, muss man einfach einige Änderungen an der makeStore -Funktion vornehmen. Insbesondere muss redux-saga innerhalb dieser Funktion und nicht außerhalb davon initialisiert werden. (Ich habe dies zunächst gemacht und einen bösen Fehler bekommen, der mir Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware .) So erreicht man genau das. Dies ist nur geringfügig aus dem Setup -Beispiel zu Beginn der Dokumente geändert. Denken Sie daran, dass dieses Setup Sie von der automatischen statischen Optimierung entschieden wird: https://err.sh/next.js/opt-out-auto-static-optimization.
Erstellen Sie Ihre Root -Saga wie gewohnt und implementieren Sie dann den Store Creator:
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 Dann in den pages/_app -Wait -Stopp -Saga und warten Sie, bis es fertig ist, wenn die Ausführung auf dem Server ist:
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 oder getStaticProps Um es mit getServerSideProps oder getStaticProps zu verwenden, müssen Sie in den Handler jeder Seite auf SAGAS 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 ( ) ;
} ) ; getInitialProps in _app Wenn Sie sich in Ihrer nächsten.js-App nicht von der automatischen Vorrenderung abmelden möchten, können Sie serverhilfe von SAGAs pro Seite wie die offiziellen nächsten.js "mit Redux-Saga" -Albild auf einer Seite verwalten. Wenn Sie sich mit dieser Option befassen, stellen Sie bitte sicher, dass Sie auf alle SAGAS in den nächsten.js -Seitenmethoden warten. Wenn Sie es auf einer der Seiten verpassen, werden inkonsistenter Zustand an den Kunden gesendet. Wir erwarten also, dass wir in _app automatisch sicherer sind, aber der Hauptnachteil ist offensichtlich, dass sich die automatischen statischen Exporte entscheiden.
Wenn Sie nur kleine Teile Ihres Staates bestehen müssen, ist das nächste Redux-Cookie-Wrapper möglicherweise eine einfache Alternative zu Redux, die SSR unterstützt.
Boilerplate: https://github.com/fazlulkarimweb/with-next-redux-wrapper-redux-persist
Ehrlich gesagt denke ich, dass es nicht notwendig ist, ein Persistenz -Tor zu setzen, da der Server bereits HTML mit einem Zustand senden kann. Es ist daher besser, es sofort zu zeigen und dann zu warten, bis die REHYDRATE zusätzlich delta zeigt, die aus dem Speicher von Persistenz stammen. Deshalb verwenden wir in erster Linie das Rendering der Serverseite.
Aber für diejenigen, die die Benutzeroberfläche tatsächlich blockieren wollen, während Rehydration stattfindet, ist hier die Lösung (aber immer noch hacky):
// 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 ,
} ) ; Und dann können Sie auf der Seite "Next.js _app mit bloßem Kontextzugriff verwenden, um den Store zu erhalten (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 >
) ;
}
} ,
) ;Oder verwenden Haken:
// 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 >
) ;
} ) ;Und dann auf der Seite 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 >
) ) ; Die Signatur von createWrapper hat sich geändert: Anstelle von createWrapper<State> sollten Sie createWrapper<Store<State>> verwenden, werden alle Typen automatisch aus Store abgeleitet.
GetServerSidePropsContext GetStaticPropsContext GetStaticPropsContext GetStaticProps nicht mehr GetServerSideProps GetServerSidePropsContext next-redux-wrapper next .
Alle Unterschriften wie ({store, req, res, ...}) => { ... } wurden in store => ({req, res, ...}) => { ... } geändert, um die nächsten Modifikationen frei zu halten.
In Version 7.x müssen Sie alle getInitialProps mit ordnungsgemäßen wrapper.getInitialPageProps manuell wrapper.getInitialAppProps .
Fenster.Next_redux_wrapper_Store wurde entfernt, da es Probleme beim heißen Nachladen verursachte
Hauptveränderung in der Art und Weise, wie die Dinge in Version 6 verpackt sind.
wrapper.withRedux(MyApp) Standard -Export const wrapper = createWrapper(makeStore, {debug: true}) withRedux ist veraltet gekennzeichnet.
Ihre makeStore -Funktion erhält nicht mehr initialState , sondern empfängt nur den Kontext: makeStore(context: Context) . Der Kontext kann NextPageContext oder AppContext oder getStaticProps oder getServerSideProps -Kontext sein, je nachdem, welche Lebenszyklusfunktion Sie einwickeln. Stattdessen müssen Sie die HYDRATE im Reduzierer behandeln. Die payload dieser Aktion enthält den state zum Zeitpunkt der statischen Erzeugung oder der Server -Seite, sodass Ihr Reduzierer ihn mit vorhandenem Client -Status ordnungsgemäß zusammenführen muss.
App sollte ihre Kinder nicht mehr mit Provider einwickeln, sondern jetzt intern.
isServer wird nicht in context / props übergeben. Verwenden Sie Ihre eigene Funktion oder einfache Überprüfung const isServer = typeof window === 'undefined' oder !!context.req oder !!context.ctx.req .
store wird nicht an umwickelte Komponenten -Requisiten übergeben.
WrappedAppProps wurde in WrapperProps umbenannt.
Wenn Ihr Projekt Next.js 5 und Next Redux -Wrapper 1.x verwendet hat, können Sie diese Anweisungen auf 2.x aktualisieren.
Upgrade als nächstes.js und Wrapper
$ npm install next@6 --save-dev
$ npm install next-redux-wrapper@latest --save Ersetzen Sie alle Verwendungen des import withRedux from "next-redux-wrapper"; und withRedux(...)(WrappedComponent) auf allen Seiten mit einfachem React Redux connect -hoc:
import { connect } from "react-redux" ;
export default connect ( ... ) ( WrappedComponent ) ;Möglicherweise müssen Sie auch Ihre auf Wrapper-Objektbasis basierende Konfiguration auf einfache React Redux-Konfiguration neu formatieren.
Erstellen Sie die pages/_app.js -Datei mit dem folgenden Minimalcode:
// 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 ) ; Befolgen Sie die Anweisungen für alle Ihre Komponenten ( props.router anstelle von props.url usw.).
Das war's. Ihr Projekt sollte jetzt genauso funktionieren wie zuvor.