Un hoc qui apporte ensuite.js et redux ensemble
Contenu:
La configuration de Redux pour les applications statiques est assez simple: un seul magasin Redux doit être créé qui est fourni à toutes les pages.
Lorsque le rendu du générateur de sites statiques ou du serveur statique est impliqué, les choses commencent à se compliquer car une autre instance de magasin est nécessaire sur le serveur pour rendre les composants connectés à Redux.
De plus, l'accès au Store Redux peut également être nécessaire lors d'une page getInitialProps .
C'est là que next-redux-wrapper est utile: il crée automatiquement les instances de magasin pour vous et s'assure qu'ils ont tous le même état.
De plus, il permet de gérer correctement les cas complexes comme App.getInitialProps (lors de l'utilisation pages/_app ) ainsi que getStaticProps ou getServerSideProps au niveau de la page individuelle.
La bibliothèque fournit une interface uniforme, quelle que soit la méthode du cycle de vie suivant.js que vous souhaitez utiliser le Store .
Dans l'exemple suivant.js https://github.com/vercel/next.js/blob/canary/examples/with-redux-thunk/store.js#l23 Le magasin est remplacé sur la navigation. Redux repensera les composants même avec des sélecteurs mémorisés ( createSelector à partir de recompose ) si store est remplacé: https://codesandbox.io/s/redux-store-change-kzs8q, ce qui peut affecter les performances de l'application en provoquant un énorme Re-Render de tout, même ce qui n'a pas changé. Cette bibliothèque s'assure que store reste le même.
npm install next-redux-wrapper react-redux --save Notez que next-redux-wrapper nécessite react-redux comme dépendance des pairs.
Exemple en direct: https://codesandbox.io/s/next-redux-wrapper-demo-7n2t5.
Tous les exemples sont écrits en dactylographie. Si vous utilisez un JavaScript simple, omettez simplement les déclarations de type. Ces exemples utilisent Vanilla Redux, si vous utilisez Redux Toolkit, veuillez vous référer à l'exemple dédié.
Next.js a plusieurs mécanismes de récupération de données, cette bibliothèque peut s'attacher à l'une d'entre elles. Mais vous devez d'abord écrire un code commun.
Veuillez noter que votre réducteur doit avoir le gestionnaire d'action HYDRATE . Le gestionnaire d'action HYDRATE doit réconcilier correctement l'état hydraté au-dessus de l'état existant (le cas échéant). Ce comportement a été ajouté dans la version 6 de cette bibliothèque. Nous parlerons de cette action spéciale plus tard.
Créer un fichier nommé 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 Il est fortement recommandé d'utiliser pages/_app pour envelopper toutes les pages à la fois, sinon en raison des conditions de course potentielles que vous pourriez obtenir 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 >
) ;
} ; Au lieu de wrapper.useWrappedStore vous pouvez également utiliser Legacy HOC, qui peut fonctionner avec des composants basés sur les classes.
getInitialProps génériques lors de l'utilisation class MyApp extends App qui sera récupérée par l'emballage, vous ne devez donc pas étendre App car vous serez retiré de l'optimisation statique automatique: https://err.sh/next.js/opt-out-auto-static-optimisation. Exportez simplement un composant fonctionnel régulier comme dans l'exemple ci-dessus.
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 ) ; Chaque fois que les pages qui ont getStaticProps ou getServerSideProps sont ouvertes par l'utilisateur, l'action HYDRATE sera expédiée. Cela peut se produire lors du chargement initial de la page et lors de la navigation régulière des pages. La payload de cette action contiendra l' state au moment de la génération statique ou du rendu côté serveur, de sorte que votre réducteur doit le fusionner correctement avec l'état client existant.
Le moyen le plus simple consiste à utiliser la séparation du serveur et de l'état client.
Une autre façon consiste à utiliser https://github.com/benjamine/jsondiffpatch pour analyser Diff et l'appliquer correctement:
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 ;
}
} ;Ou comme ceci (à partir de l'exemple with-reux-wrapper dans le repo suivant.js):
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 ) ;
}
} ; La fonction createWrapper accepte makeStore comme son premier argument. La fonction makeStore doit renvoyer une nouvelle instance Store Redux à chaque fois qu'elle s'appelle. Aucune création n'est nécessaire ici, elle se fait automatiquement à l'intérieur de l'emballage.
createWrapper accepte également éventuellement un objet de configuration comme un deuxième paramètre:
debug (facultatif, booléen): activer la journalisation de débogageserializeState et deserializeState : fonctions personnalisées pour sérialiser et désérialiser l'état redux, voir sérialisation personnalisée et désérialisation. Lorsque makeStore est invoqué, il est fourni avec un contexte suivant.js, qui pourrait être le contexte NextPageContext ou AppContext ou getStaticProps ou getServerSideProps en fonction de la fonction de cycle de vie que vous allez envelopper.
Certains de ces contextes ( getServerSideProps toujours, et NextPageContext , AppContext Parfois si la page est rendue sur serveur) peut avoir des propriétés liées à la demande et à la réponse:
req ( IncomingMessage )res ( ServerResponse ) Bien qu'il soit possible de créer une logique spécifique au serveur ou au client dans makeStore , je recommande fortement qu'ils n'aient pas de comportement différent. Cela peut entraîner des erreurs et des décalages de somme de contrôle qui, à leur tour, ruineront l'objectif du rendu du serveur.
Cette section décrit comment attacher pour obtenir la fonction de cycle de vie GetStaticProps.
Créons une page dans 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 sont ouvertes par l'utilisateur, l'action HYDRATE sera envoyée. La payload de cette action contiendra l' state au moment de la génération statique, il n'aura pas d'état client, donc votre réducteur doit le fusionner avec l'état client existant. En savoir plus à ce sujet dans la séparation du serveur et de l'état client.
Bien que vous puissiez envelopper des pages individuelles (et ne pas envelopper les pages/_app ), il n'est pas recommandé, voir le dernier paragraphe dans la section d'utilisation.
Cette section décrit comment se connecter à la fonction de cycle de vie de GetSerVersideProps.
Créons une page dans 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 sont ouvertes par l'utilisateur, l'action HYDRATE sera envoyée. La payload de cette action contiendra l' state au moment du rendu côté serveur, il n'aura pas d'état client, donc votre réducteur doit le fusionner correctement avec l'état client existant. En savoir plus à ce sujet dans la séparation du serveur et de l'état client.
Bien que vous puissiez envelopper des pages individuelles (et ne pas envelopper les pages/_app ), il n'est pas recommandé, voir le dernier paragraphe dans la section d'utilisation.
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 ; Gardez à l'esprit que req et res peuvent ne pas être disponibles si getInitialProps est appelé côté client.
Le composant de fonction sans état peut également être remplacé par la classe:
class Page extends Component {
public static getInitialProps = wrapper . getInitialPageProps ( store => ( ) => ( { ... } ) ) ;
render ( ) {
// stuff
}
}
export default Page ; Bien que vous puissiez envelopper des pages individuelles (et ne pas envelopper les pages/_app ), il n'est pas recommandé, voir le dernier paragraphe dans la section d'utilisation.
pages/_app . Mais ce mode n'est pas compatible avec la fonction d'exportation statique Auto Partial d'Auto.JS 9, voir l'explication ci-dessous.
Le wrapper peut également être attaché à votre composant _app (situé dans /pages ). Tous les autres composants peuvent utiliser la fonction connect de react-redux .
// 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 ) ;Alors toutes les pages peuvent simplement être connectées (l'exemple considère les composants de la page):
// 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 ou getStaticProps au niveau de la page Vous pouvez également utiliser getServerSideProps ou getStaticProps au niveau de la page, dans ce cas, l'action HYDRATE sera envoyée deux fois: avec état après App.getInitialProps , puis avec un état après getServerSideProps ou getStaticProps :
getServerSideProps au niveau de la page, store dans getServerSideProps sera exécuté après App.getInitialProps et en aura un état, donc le deuxième HYDRATE aura un état complet des deuxgetStaticProps au niveau de la page, store dans getStaticProps sera exécuté au moment de la compilation et n'aura pas d'état de App.getInitialProps car ils sont exécutés dans différents contextes et l'état ne peut pas être partagé. Les premières actions HYDRATE indiquent App.getInitialProps et Second auront un état après getStaticProps (même s'il a été exécuté plus tôt dans le temps). Le moyen le plus simple de garantir la fusion appropriée consiste à supprimer les valeurs initiales de action.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 ;
}
} ; Supposons que la page envoie uniquement l'action PAGE et l' APP uniquement, cela rend la fusion d'état sûre.
En savoir plus à ce sujet dans la séparation du serveur et de l'état client.
À l'aide de next-redux-wrapper ("The Wrapper"), les choses suivantes se produisent sur une demande:
Phase 1: getInitialProps / getStaticProps / getServerSideProps
makeStore ) avec un état initial vide. Ce faisant, il fournit également les objets Request et Response comme options à makeStore .getInitialProps de _app et passe la boutique précédemment créée.getInitialProps de _app , ainsi que l'état du magasin.getXXXProps de la page et passe la boutique précédemment créée.getXXXProps de la page, ainsi que l'état du magasin.Phase 2: SSR
makeStoreHYDRATE avec l'état du magasin précédent comme payload_app ou page .Phase 3: Client
HYDRATE avec l'état de la phase 1 comme payload_app ou page .Remarque: L'état du client n'est pas persisté sur les demandes (c'est-à-dire que la phase 1 commence toujours par un état vide). Par conséquent, il est réinitialisé sur les recharges de page. Envisagez d'utiliser Redux Persiste si vous voulez persister l'état entre les demandes.
Depuis la prise en charge de la version 7.0 de première classe de @reduxjs/toolkit a été ajoutée.
Exemple complet: https://github.com/kill-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 ] ; Il est recommandé d'exporter State dactylographié et ThunkAction :
export type AppStore = ReturnType < typeof makeStore > ;
export type AppState = ReturnType < AppStore [ 'getState' ] > ;
export type AppThunk < ReturnType = void > = ThunkAction < ReturnType , AppState , unknown , Action > ; Chaque fois que les pages qui ont getStaticProps ou getServerSideProps sont ouvertes par l'utilisateur, l'action HYDRATE sera expédiée. La payload de cette action contiendra l' state au moment de la génération statique ou du rendu côté serveur, de sorte que votre réducteur doit le fusionner correctement avec l'état client existant.
Le moyen le plus simple et le plus stable de s'assurer que rien n'est accidentellement écrasé est de vous assurer que votre réducteur applique les actions côté client et le côté serveur à différents sous-états de votre état et qu'ils ne s'affrontent jamais:
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 ;
}
} ; Si vous préférez une approche isomorphe pour certaines parties (de préférence petites) de votre état, vous pouvez les partager entre le client et le serveur sur des pages de serveur à l'aide de Next-Redux-Cookie-Wrapper, une extension de la prochaine-Redux-WAPPER. Dans ce cas, pour les sous-états sélectionnés, le serveur est au courant de l'état du client (sauf dans getStaticProps ) et il n'est pas nécessaire de séparer le serveur et l'état client.
En outre, vous pouvez utiliser une bibliothèque comme https://github.com/benjamine/jsondiffpatch pour analyser Diff et l'appliquer correctement.
Je ne recommande pas d'utiliser withRedux en pages/_document.js , next.js ne fournit pas de moyen fiable de déterminer la séquence lorsque les composants seront rendus. Donc, par recommandation Next.js, il est préférable d'avoir juste des choses auto-agtiques dans pages/_document .
Les pages d'erreur peuvent également être enveloppées de la même manière que toutes les autres pages.
La transition vers une page d'erreur (modèle pages/_error.js ) entraînera l'application pages/_app.js , mais il s'agit toujours d'une transition pleine page (pas de HTML5 Pushstate), donc le client aura le magasin créé à partir de zéro à l'aide de l'état du serveur. Donc, à moins que vous ne persistiez le magasin sur le client d'une manière ou d'une autre, l'état client précédent résultant sera ignoré.
Vous pouvez utiliser https://github.com/reduxjs/redux-thunk pour expédier des actions asynchrones:
function someAsyncAction ( id ) {
return async function ( dispatch , getState ) {
return someApiCall ( id ) . then ( res => {
dispatch ( {
type : 'FOO' ,
payload : res ,
} ) ;
} ) ;
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ;Vous pouvez également installer https://github.com/pburtchaell/redux-promise-middleware afin d'envoyer des promesses en tant qu'actions asynchrones. Suivez le guide d'installation de la bibliothèque, alors vous pourrez le gérer comme ceci:
function someAsyncAction ( ) {
return {
type : 'FOO' ,
payload : new Promise ( resolve => resolve ( 'foo' ) ) ,
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ; Si vous stockez des types de complexes tels que Immutable.js ou JSON objets dans votre état, un gestionnaire de sérialisation et de désérialisation personnalisés peut être pratique pour sérialiser l'état Redux sur le serveur et le désérialiser à nouveau sur le client. Pour ce faire, fournissez serializeState et deserializeState comme options de configuration à withRedux .
La raison en est que l'état d'instantané est transféré sur le réseau du serveur vers le client en tant qu'objet simple.
Exemple de sérialisation personnalisée d'un état immuable.js à l'aide de json-immutable :
const { serialize , deserialize } = require ( 'json-immutable' ) ;
createWrapper ( {
serializeState : state => serialize ( state ) ,
deserializeState : state => deserialize ( state ) ,
} ) ;Même chose en utilisant immuable.js:
const { fromJS } = require ( 'immutable' ) ;
createWrapper ( {
serializeState : state => state . toJS ( ) ,
deserializeState : state => fromJS ( state ) ,
} ) ; [Remarque, cette méthode peut être dangereuse - assurez-vous de réfléchir correctement à la gestion des sagas asynchrones. Les conditions de course se produisent très facilement si vous ne faites pas attention.] Pour utiliser la saga Redux, il faut simplement apporter des modifications à leur fonction makeStore . Plus précisément, redux-saga doit être initialisé à l'intérieur de cette fonction, plutôt que de l'extérieur. (Je l'ai fait au début et j'ai eu une mauvaise erreur me disant Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware ). Voici comment on accomplit juste cela. Ceci est juste légèrement modifié à partir de l'exemple de configuration au début des documents. Gardez à l'esprit que cette configuration vous retirera de l'optimisation statique automatique: https://err.sh/next.js/opt-out-auto-static-optimisation.
Créez votre saga racine comme d'habitude, puis implémentez le créateur de magasin:
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 Ensuite, dans les pages/_app , arrêtez la saga et attendez qu'il termine lorsque l'exécution est sur le serveur:
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 ou getStaticProps Afin de l'utiliser avec getServerSideProps ou getStaticProps vous devez await pour les sagas dans le gestionnaire de chaque page:
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 à l'intérieur _app Si vous ne souhaitez pas désactiver le pré-rendu automatique dans votre application Next.js, vous pouvez gérer les sagas appelés au serveur sur une base par page comme l'exemple officiel Next.js "avec redux Saga" fait. Si vous allez avec cette option, veuillez vous assurer que vous attendez toutes les sagas dans toutes les méthodes de page suivantes. Si vous le manquez sur l'une des pages, vous vous retrouverez avec un état incohérent envoyé au client. Nous envisageons donc d'attendre dans _app d'être automatiquement plus sûr, mais évidemment, l'inconvénient principal est de se retirer des exportations statiques automatiques.
Si vous avez seulement besoin de persister de petites parties de votre état, le prochain-Redux-Cookie-Warpper pourrait être une alternative facile à Redux persistant qui prend en charge la SSR.
Plate-plaque: https://github.com/fazlulkarimweb/with-next-redux-wrapper-redux-persiste
Honnêtement, je pense que la mise en place d'une porte de persistance n'est pas nécessaire car le serveur peut déjà envoyer du HTML avec un état, il est donc préférable de le montrer immédiatement et d'attendre que l'action REHYDRATE montre un delta supplémentaire provenant du stockage de persistance. C'est pourquoi nous utilisons le rendu côté serveur en premier lieu.
Mais, pour ceux qui veulent réellement bloquer l'interface utilisateur pendant que la réhydratation se produit, voici la solution (toujours 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 ,
} ) ; Et puis dans la page suivante.js _app , vous pouvez utiliser un accès de contexte nu pour obtenir le magasin (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 >
) ;
}
} ,
) ;Ou en utilisant des crochets:
// 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 >
) ;
} ) ;Puis dans la page suivante.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 >
) ) ; La signature de createWrapper a changé: au lieu de createWrapper<State> vous devez utiliser createWrapper<Store<State>> , tous les types seront automatiquement déduits du Store .
GetServerSidePropsContext et GetStaticPropsContext ne sont plus exportés à partir de next-redux-wrapper , vous devriez utiliser GetServerSideProps , GetServerSidePropsContext , GetStaticProps et GetStaticPropsContext directement à partir de next .
Toutes les signatures comme ({store, req, res, ...}) => { ... } ont été changées en store => ({req, res, ...}) => { ... } afin de garder les internes suivants.
Dans la version 7.x vous devez envelopper manuellement tous getInitialProps avec des emballages appropriés: wrapper.getInitialPageProps et wrapper.getInitialAppProps .
window.next_redux_wrapper_store a été supprimée car elle causait des problèmes avec le rechargement chaud
Changement majeur dans la façon dont les choses sont enveloppées dans la version 6.
L'exportation par défaut withRedux est marquée dépréciée, vous devez créer un wrapper const wrapper = createWrapper(makeStore, {debug: true}) , puis utiliser wrapper.withRedux(MyApp) .
Votre fonction makeStore n'obtient plus d' initialState , il ne reçoit que le contexte: makeStore(context: Context) . Le contexte peut être NextPageContext ou AppContext ou getStaticProps ou getServerSideProps Contexte en fonction de la fonction de cycle de vie que vous allez envelopper. Au lieu de cela, vous devez gérer l'action HYDRATE dans le réducteur. La payload de cette action contiendra l' state au moment de la génération statique ou du rendu côté serveur, de sorte que votre réducteur doit le fusionner correctement avec l'état client existant.
App ne doit plus envelopper ses enfants avec Provider , cela se fait maintenant en interne.
isServer n'est pas adopté dans context / props , utilisez votre propre fonction ou simple vérification const isServer = typeof window === 'undefined' ou !!context.req ou !!context.ctx.req .
store n'est pas transmis aux accessoires composants enveloppés.
WrappedAppProps a été renommé WrapperProps .
Si votre projet utilisait Next.js 5 et Next Redux Wrapper 1.x Ces instructions vous aideront à passer à 2.x.
Mettre à niveau next.js et emballage
$ npm install next@6 --save-dev
$ npm install next-redux-wrapper@latest --save Remplacez toutes les usages d' import withRedux from "next-redux-wrapper"; et withRedux(...)(WrappedComponent) dans toutes vos pages avec un readd redux connect hoc:
import { connect } from "react-redux" ;
export default connect ( ... ) ( WrappedComponent ) ;Vous devrez peut-être également reformater votre configuration basée sur les objets en wrapper vers une configuration React Redux simple.
Créez le fichier pages/_app.js avec le code minimal suivant:
// 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 ) ; Suivez Next.js 6 Instructions de mise props.url props.router
C'est ça. Votre projet devrait maintenant fonctionner la même chose qu'auparavant.