Un hoc que reúne a los próximos.js y Redux
Contenido:
Configurar Redux para aplicaciones estáticas es bastante simple: se debe crear una sola tienda Redux que se proporcione a todas las páginas.
Sin embargo, cuando la representación del generador de sitios estático o el lado del servidor está involucrada, las cosas comienzan a complicarse a medida que se necesita otra instancia de la tienda en el servidor para representar componentes conectados a Redux.
Además, el acceso a la Store Redux también puede ser necesario durante getInitialProps de una página.
Aquí es donde next-redux-wrapper es útil: automáticamente crea las instancias de la tienda para usted y se asegura de que todos tengan el mismo estado.
Además, permite manejar correctamente casos complejos como App.getInitialProps (al usar pages/_app ) junto con getStaticProps o getServerSideProps a nivel de página individual.
La biblioteca proporciona una interfaz uniforme sin importar en el que el método de ciclo de vida de Next.js le gustaría usar la Store .
En el ejemplo de Next.js https://github.com/vercel/next.js/blob/canary/examples/with-redux-thunk/store.js#l23 La tienda se está reemplazando en la navegación. Redux volverá a renderizar los componentes incluso con selectores memoados ( createSelector de recompose ) si store se reemplaza: https://codesandbox.io/s/redux-store-change-kzs8q, lo que puede afectar el rendimiento de la aplicación al causar un gran re-render de todo, incluso lo que no cambió. Esta biblioteca asegura que store siga siendo la misma.
npm install next-redux-wrapper react-redux --save Tenga en cuenta que next-redux-wrapper requiere react-redux como dependencia de pares.
Ejemplo en vivo: https://codesandbox.io/s/next-redux-wrapper-demo-7n2t5.
Todos los ejemplos están escritos en TypeScript. Si está usando JavaScript simple, solo omita las declaraciones de tipo. Estos ejemplos usan Vanilla Redux, si está utilizando Redux Toolkit, consulte el ejemplo dedicado.
Next.js tiene varios mecanismos de recuperación de datos, esta biblioteca puede adjuntar a cualquiera de ellos. Pero primero tienes que escribir algún código común.
Tenga en cuenta que su reductor debe tener el controlador de acción HYDRATE . El controlador de acción HYDRATE debe reconciliar adecuadamente el estado hidratado sobre el estado existente (si lo hay). Este comportamiento se agregó en la versión 6 de esta biblioteca. Hablaremos de esta acción especial más tarde.
Cree un archivo llamado 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 Se recomienda altamente usar pages/_app para envolver todas las páginas a la vez, de lo contrario, debido a posibles condiciones de carrera que puede obtener 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 >
) ;
} ; En lugar de wrapper.useWrappedStore también puede usar Legacy HOC, que puede funcionar con componentes basados en clases.
getInitialProps genérico cuando use class MyApp extends App que será recogida por Wrapper, por lo que no debe extender App , ya que se opta por la optimización estática automática: https://err.sh/next.js/opt-ut-auto-static-optimización. Simplemente exporte un componente funcional regular como en el ejemplo anterior.
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 ) ; Cada vez que las páginas que tengan getStaticProps o getServerSideProps se abran por el usuario, se enviará la acción HYDRATE . Esto puede suceder durante la carga inicial de la página y durante la navegación de la página regular. La payload de esta acción contendrá el state en el momento de la generación estática o la representación del lado del servidor, por lo que su reductor debe fusionarlo con el estado cliente existente correctamente.
La forma más simple es usar la separación del estado del servidor y el cliente.
Otra forma es usar https://github.com/benjamine/jsondiffpatch para analizar Diff y aplicarlo correctamente:
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 ;
}
} ;O así (del ejemplo con Redux-Wrapper en 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 ) ;
}
} ; La función createWrapper acepta makeStore como su primer argumento. La función makeStore debe devolver una nueva instancia de Redux Store cada vez que se llama. Aquí no se necesita memoización, se realiza automáticamente dentro del envoltorio.
createWrapper también opcionalmente acepta un objeto de configuración como segundo parámetro:
debug (opcional, booleano): habilitar el registro de depuraciónserializeState y deserializeState : funciones personalizadas para serializar y deserializar el estado de Redux, ver serialización y deserialización personalizadas. Cuando se invoca makeStore , se proporciona con un contexto Next.js, que podría ser NextPageContext o AppContext o getStaticProps o getServerSideProps Context, dependiendo de qué función del ciclo de vida envuelva.
Algunos de esos contextos ( getServerSideProps siempre, y NextPageContext , AppContext a veces si la página se representa en el servidor) pueden tener propiedades relacionadas con la solicitud y respuesta:
req ( IncomingMessage )res ( ServerResponse ) Aunque es posible crear una lógica específica del servidor o cliente en ambos makeStore , recomiendo que no tengan un comportamiento diferente. Esto puede causar errores y desajustes de suma de verificación que a su vez arruinarán todo el propósito de la representación del servidor.
Esta sección describe cómo adjuntar a la función de ciclo de vida de GetstaticProps.
Creemos una página en 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 , se enviará la acción HYDRATE . La payload de esta acción contendrá el state en el momento de la generación estática, no tendrá el estado del cliente, por lo que su reductor debe fusionarlo con el estado cliente existente correctamente. Más sobre esto en la separación del servidor y el estado del cliente.
Aunque puede envolver páginas individuales (y no envolver las pages/_app ), no se recomienda, consulte el último párrafo en la sección de uso.
Esta sección describe cómo adjuntar a la función de ciclo de vida de GetServersideProps.
Creemos una página en 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 , se enviará la acción HYDRATE . La payload de esta acción contendrá el state en el momento de la representación del lado del servidor, no tendrá el estado del cliente, por lo que su reductor debe fusionarlo con el estado cliente existente correctamente. Más sobre esto en la separación del servidor y el estado del cliente.
Aunque puede envolver páginas individuales (y no envolver las pages/_app ), no se recomienda, consulte el último párrafo en la sección de uso.
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 ; Tenga en cuenta que req y res pueden no estar disponibles si getInitialProps se llama al lado del cliente.
El componente de la función sin estado también se puede reemplazar con la clase:
class Page extends Component {
public static getInitialProps = wrapper . getInitialPageProps ( store => ( ) => ( { ... } ) ) ;
render ( ) {
// stuff
}
}
export default Page ; Aunque puede envolver páginas individuales (y no envolver las pages/_app ), no se recomienda, consulte el último párrafo en la sección de uso.
pages/_app . Pero este modo no es compatible con la función de exportación estática automática de Next.js 9, vea la explicación a continuación.
El envoltorio también se puede conectar a su componente _app (ubicado en /pages ). Todos los demás componentes pueden usar la función 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 ) ;Entonces todas las páginas se pueden conectar simplemente (el ejemplo considera los componentes de la página):
// 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 o getStaticProps a nivel de página También puede usar getServerSideProps o getStaticProps a nivel de página, en este caso, HYDRATE Action se enviará dos veces: con estado después de App.getInitialProps y luego con el estado después de getServerSideProps o getStaticProps :
getServerSideProps a nivel de página HYDRATE entonces store en getServerSideProps se ejecutará después de App.getInitialProps .getStaticProps a nivel de página, store en getStaticProps se ejecutará en el momento de la compilación y no tendrá estado de App.getInitialProps porque se ejecutan en diferentes contextos y el estado no se puede compartir. El primer estado de las acciones HYDRATE después de App.getInitialProps y el segundo tendrán estado después de getStaticProps (a pesar de que se ejecutó antes en el tiempo). La forma más simple de garantizar la fusión adecuada es eliminar los valores iniciales 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 ;
}
} ; Suponga que la página solo envía acción PAGE y aplicación de solo APP , esto hace que la fusión estatal sea segura.
Más sobre eso en la separación del servidor y el estado del cliente.
Usando next-redux-wrapper ("The Wrapper"), las siguientes cosas suceden en una solicitud:
Fase 1: getInitialProps / getStaticProps / getServerSideProps
makeStore ) con un estado inicial vacío. Al hacerlo, también proporciona los objetos Request y Response como opciones para makeStore .getInitialProps de _app y pasa la tienda creada anteriormente.getInitialProps de _app , junto con el estado de la tienda.getXXXProps de la página y pasa la tienda creada anteriormente.getXXXProps de la página, junto con el estado de la tienda.Fase 2: SSR
makeStoreHYDRATE con el estado de la tienda anterior como payload_app o page .Fase 3: Cliente
HYDRATE con el estado desde la fase 1 como payload_app o page .Nota: El estado del cliente no se persiste en todas las solicitudes (es decir, la fase 1 siempre comienza con un estado vacío). Por lo tanto, se restablece en las recargas de la página. Considere usar Redux Persisting si desea persistir entre solicitudes.
Desde la versión 7.0 se ha agregado el soporte de primera clase de @reduxjs/toolkit .
Ejemplo completo: 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 ] ; Se recomienda exportar State escrito y ThunkAction :
export type AppStore = ReturnType < typeof makeStore > ;
export type AppState = ReturnType < AppStore [ 'getState' ] > ;
export type AppThunk < ReturnType = void > = ThunkAction < ReturnType , AppState , unknown , Action > ; Cada vez que las páginas que tengan getStaticProps o getServerSideProps se abran por el usuario, se enviará la acción HYDRATE . La payload de esta acción contendrá el state en el momento de la generación estática o la representación del lado del servidor, por lo que su reductor debe fusionarlo con el estado cliente existente correctamente.
La forma más fácil y estable de asegurarse de que nada se sobrescribe accidentalmente es asegurarse de que su reductor aplique acciones del lado del cliente y del lado del servidor a diferentes subestaciones de su estado y nunca chocen:
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 prefiere un enfoque isomórfico para algunas porciones (preferiblemente pequeñas) de su estado, puede compartirlos entre el cliente y el servidor en las páginas renderizadas por el servidor utilizando el próximo-Redux-Cookie-Wrapper, una extensión al siguiente-Redux-Wrapper. En este caso, para los subestados seleccionados, el servidor es consciente del estado del cliente (a menos que en getStaticProps ) y no hay necesidad de separar el estado del servidor y el cliente.
Además, puede usar una biblioteca como https://github.com/benjamine/jsondiffpatch para analizar la diferencia y aplicarla correctamente.
No recomiendo usar withRedux en pages/_document.js , Next.js no proporciona una forma confiable de determinar la secuencia cuando los componentes se representarán. Entonces, según la recomendación de Next.js, es mejor tener solo cosas agnósticas de datos en pages/_document .
Las páginas de error también se pueden envolver de la misma manera que cualquier otra página.
La transición a una página de error (plantilla de pages/_error.js ) hará que se apliquen pages/_app.js , pero siempre es una transición de página completa (no HTML5 PushState), por lo que el cliente tendrá la tienda creada desde cero usando el estado desde el servidor. Entonces, a menos que persista la tienda en el cliente de alguna manera se ignorará el estado del cliente anterior resultante.
Puede usar https://github.com/reduxjs/redux-thunk para enviar acciones de async:
function someAsyncAction ( id ) {
return async function ( dispatch , getState ) {
return someApiCall ( id ) . then ( res => {
dispatch ( {
type : 'FOO' ,
payload : res ,
} ) ;
} ) ;
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ;También puede instalar https://github.com/pburtchaell/redux-promise-middleware para enviar promesas como acciones async. Siga la guía de instalación de la biblioteca, luego podrá manejarla así:
function someAsyncAction ( ) {
return {
type : 'FOO' ,
payload : new Promise ( resolve => resolve ( 'foo' ) ) ,
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ; Si está almacenando tipos complejos como inmutables.js o objetos JSON en su estado, un controlador de serialización y deserialización personalizados podría ser útil para serializar el estado Redux en el servidor y deserializarlo nuevamente en el cliente. Para hacerlo, proporcione serializeState y deserializeState como opciones de configuración para withRedux .
La razón es que la instantánea de estado se transfiere a través de la red de servidor a cliente como un objeto simple.
Ejemplo de una serialización personalizada de un estado inmutable.js usando json-immutable :
const { serialize , deserialize } = require ( 'json-immutable' ) ;
createWrapper ( {
serializeState : state => serialize ( state ) ,
deserializeState : state => deserialize ( state ) ,
} ) ;Lo mismo usando inmutable.js:
const { fromJS } = require ( 'immutable' ) ;
createWrapper ( {
serializeState : state => state . toJS ( ) ,
deserializeState : state => fromJS ( state ) ,
} ) ; [Tenga en cuenta que este método puede ser inseguro: asegúrese de pensar mucho en el manejo de las sagas async correctamente. Las condiciones de carrera ocurren muy fácilmente si no tienes cuidado.] Para utilizar Redux Saga, uno simplemente tiene que hacer algunos cambios en su función makeStore . Específicamente, redux-saga debe inicializarse dentro de esta función, en lugar de fuera de ella. (Al principio hice esto y obtuve un error desagradable que me decía Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware ). Así es como uno logra solo eso. Esto se modifica ligeramente del ejemplo de configuración al comienzo de los documentos. Tenga en cuenta que esta configuración le optará por la optimización estática automática: https://err.sh/next.js/opt-ut-auto-static-optimization.
Cree su saga raíz como de costumbre, luego implementa el creador de la tienda:
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 Luego, en las pages/_app espera, pare la saga y espere a que termine cuando la ejecución esté en el servidor:
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 o getStaticProps Para usarlo con getServerSideProps o getStaticProps , debe await para las sagas en el controlador de cada página:
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 dentro _app Si no desea optar por no participar en la referencia automática en su aplicación Next.js, puede administrar las sagas llamadas de servidor por página como el ejemplo oficial de Next.js "con Redux Saga". Si va con esta opción, asegúrese de esperar todas y cada una de las sagas dentro de los métodos de página Next.js. Si lo pierde en una de las páginas, terminará con un estado inconsistente que se envía al cliente. Por lo tanto, consideramos que esperar en _app es automáticamente más seguro, pero obviamente el principal inconveniente es optar por las exportaciones estáticas automáticas.
Si solo necesita persistir pequeñas porciones de su estado, el próximo-Redux-Cookie-Wrapper podría ser una alternativa fácil a Redux persistir que admite SSR.
Boilerplate: https://github.com/fazlulkarimweb/with-next-redux-wraper-rapsist
Honestamente, creo que poner una puerta de persistencia no es necesaria porque el servidor ya puede enviar un poco de HTML con algún estado, por lo que es mejor mostrarla de inmediato y luego esperar a que la acción REHYDRATE suceda para mostrar delta adicional proveniente del almacenamiento de persistencia. Es por eso que usamos la representación del lado del servidor en primer lugar.
Pero, para aquellos que realmente quieren bloquear la interfaz de usuario mientras está ocurriendo la rehidratación, aquí está la solución (aunque todavía 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 ,
} ) ; Y luego en la página Next.js _app puede usar el acceso de contexto desnudo para obtener la tienda (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 >
) ;
}
} ,
) ;O usando ganchos:
// 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 >
) ;
} ) ;Y luego en la página 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 >
) ) ; La firma de createWrapper ha cambiado: en lugar de createWrapper<State> debe usar createWrapper<Store<State>> , todos los tipos se inferirán automáticamente de Store .
GetServerSidePropsContext y GetStaticPropsContext ya no se exportan desde next-redux-wrapper , debe usar GetServerSideProps , GetServerSidePropsContext , GetStaticProps y GetStaticPropsContext directamente desde next .
Todas las firmas como ({store, req, res, ...}) => { ... } se cambiaron a store => ({req, res, ...}) => { ... } para mantener a continuación. JS INTERALES libres de modificaciones y para un mejor soporte de tipificaciones.
En la versión 7.x debe envolver manualmente todos getInitialProps con envoltorios adecuados: wrapper.getInitialPageProps y wrapper.getInitialAppProps .
Window.next_redux_wrapper_store se ha eliminado, ya que estaba causando problemas con la recarga en caliente
Cambio importante en la forma en que se envuelven las cosas en la versión 6.
La exportación predeterminada withRedux está marcada en desuso, debe crear una const wrapper = createWrapper(makeStore, {debug: true}) y luego usar wrapper.withRedux(MyApp) .
Su función makeStore ya no obtiene initialState , solo recibe el contexto: makeStore(context: Context) . El contexto podría ser NextPageContext o AppContext o getStaticProps o getServerSideProps Context Dependiendo de qué función del ciclo de vida envuelva. En cambio, debe manejar la acción HYDRATE en el reductor. La payload de esta acción contendrá el state en el momento de la generación estática o la representación del lado del servidor, por lo que su reductor debe fusionarlo con el estado cliente existente correctamente.
App ya no debe envolver a sus hijos con Provider , ahora se hace internamente.
isServer no se pasa en context / props , use su propia función o comprobación simple const isServer = typeof window === 'undefined' o !!context.req o !!context.ctx.req .
store no se pasa a accesorios de componentes envueltos.
WrappedAppProps pasó a llamarse WrapperProps .
Si su proyecto estaba utilizando Next.js 5 y Next Redux Wrapper 1.x, estas instrucciones le ayudarán a actualizar a 2.x.
Actualizar Next.js y envoltura
$ npm install next@6 --save-dev
$ npm install next-redux-wrapper@latest --save Reemplace todos los usos de import withRedux from "next-redux-wrapper"; y withRedux(...)(WrappedComponent) en todas sus páginas con Plain React Redux connect HOC:
import { connect } from "react-redux" ;
export default connect ( ... ) ( WrappedComponent ) ;También es posible que tenga que reformatear su configuración basada en objetos de envoltura para una configuración simple de React Redux.
Cree el archivo de pages/_app.js con el siguiente código mínimo:
// 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 ) ; Siga Next.js 6 Instrucciones de actualización para todos sus componentes ( props.router en lugar de props.url .
Eso es todo. Su proyecto ahora debería funcionar igual que antes.