Um hoc que reúne o próximo.js e redux juntos
Conteúdo:
A configuração do Redux para aplicativos estáticos é bastante simples: uma única loja Redux deve ser criada que é fornecida a todas as páginas.
Quando o Next.JS está estático gerador de site ou renderização do lado do servidor, no entanto, as coisas começam a ficar complicadas à medida que outra instância da loja é necessária no servidor para renderizar componentes conectados ao Redux.
Além disso, o acesso ao Store Redux também pode ser necessário durante getInitialProps de uma página.
É aqui que next-redux-wrapper é útil: ele cria automaticamente as instâncias da loja para você e garante que todos tenham o mesmo estado.
Além disso, permite lidar adequadamente com casos complexos como App.getInitialProps (ao usar pages/_app ) juntamente com getStaticProps ou getServerSideProps no nível de página individual.
A biblioteca fornece uma interface uniforme, independentemente do método do ciclo de vida Next.js que você gostaria de usar a Store .
No próximo.JS Exemplo https://github.com/vercel/next.js/blob/canary/examples/with-ruredux-thunk/store.js#l23 A loja está sendo substituída na navegação. O Redux voltará a renderizar componentes mesmo com seletores memorizados ( createSelector da recompose ) se store for substituído: https://codesandbox.io/s/redux-store-change-kzs8q, o que pode afetar o desempenho do aplicativo, causando uma enorme renderização de tudo, mesmo que não mudou. Esta biblioteca garante que store permaneça a mesma.
npm install next-redux-wrapper react-redux --save Observe que o next-redux-wrapper requer react-redux como dependência de pares.
Exemplo ao vivo: https://codesandbox.io/s/next-redux-wrapper-demo-7n2t5.
Todos os exemplos são escritos no TypeScript. Se você estiver usando JavaScript simples, apenas omitisse declarações do tipo. Esses exemplos usam o Vanilla Redux, se você estiver usando o Redux Toolkit, consulte o exemplo dedicado.
Next.js possui vários mecanismos de busca de dados, esta biblioteca pode anexar a qualquer um deles. Mas primeiro você precisa escrever algum código comum.
Observe que seu redutor deve ter o manipulador de ação HYDRATE . O manipulador de ação HYDRATE deve reconciliar adequadamente o estado hidratado no topo do estado existente (se houver). Esse comportamento foi adicionado na versão 6 desta biblioteca. Falaremos sobre esta ação especial mais tarde.
Crie um arquivo chamado 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 É altamente recomendável usar pages/_app para embrulhar todas as páginas de uma só vez, caso contrário, devido a possíveis condições de corrida que você pode obter 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 >
) ;
} ; Em vez do wrapper.useWrappedStore você também pode usar o Hoc Legacy, que pode funcionar com componentes baseados em classe.
getInitialProps genéricos ao usar class MyApp extends App , que será recolhido pelo wrapper, para que você não estenda estender App , pois será optado pela otimização estática automática: https://err.sh/next.js/opt-out --uto-tatic-tatimization. Basta exportar um componente funcional regular, como no exemplo acima.
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 as páginas que possuem getStaticProps ou getServerSideProps são abertas pelo usuário, a ação HYDRATE será despachada. Isso pode acontecer durante a carga inicial da página e durante a navegação regular da página. A payload dessa ação conterá o state no momento da geração estática ou da renderização do lado do servidor, para que seu redutor deve mesclá -lo com o estado do cliente existente corretamente.
A maneira mais simples é usar a separação de estado do servidor e do cliente.
Outra maneira é usar https://github.com/benjamine/jsondiffpatch para analisar o DIFF e aplicá -lo corretamente:
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 assim (do exemplo do With-Redux-Wrapper no 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 ) ;
}
} ; A função createWrapper aceita makeStore como seu primeiro argumento. A função makeStore deve retornar uma nova instância do Redux Store sempre que é chamada. Nenhuma memórias é necessária aqui, é feita automaticamente dentro do invólucro.
createWrapper também aceita opcionalmente um objeto de configuração como um segundo parâmetro:
debug (opcional, booleano): Ativar o loggging de depuraçãoserializeState e deserializeState : Funções personalizadas para serializar e desserializar o estado Redux, consulte a serialização e a deserialização personalizadas. Quando makeStore é chamado, ele recebe um contexto do próximo.js, que pode ser NextPageContext ou AppContext ou getStaticProps ou getServerSideProps , dependendo da função do ciclo de vida que você envolverá.
Alguns desses contextos ( getServerSideProps sempre e NextPageContext , AppContext às vezes se a página for renderizada no servidor) pode ter propriedades relacionadas à solicitação e resposta:
req ( IncomingMessage )res ( ServerResponse ) Embora seja possível criar lógica específica para servidor ou cliente em ambos os makeStore , eu recomendo que eles não tenham comportamento diferente. Isso pode causar erros e incompatibilidades de soma de verificação que, por sua vez, arruinarão todo o objetivo da renderização do servidor.
Esta seção descreve como anexar à função GetStaticProps Lifecycle.
Vamos criar uma página em 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 são abertas pelo usuário, a ação HYDRATE será despachada. A payload dessa ação conterá o state no momento da geração estática, ela não terá estado de cliente, portanto seu redutor deve mesclá -lo com o estado do cliente existente corretamente. Mais sobre isso na separação de estados de servidor e cliente.
Embora você possa embrulhar páginas individuais (e não enrolar as pages/_app ), não é recomendado, consulte a seção Última parágrafo na seção de uso.
Esta seção descreve como anexar à função GetServerSideProps Lifecycle.
Vamos criar uma página em 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 são abertas pelo usuário, a ação HYDRATE será despachada. A payload dessa ação conterá o state no momento da renderização do lado do servidor, ela não terá estado de cliente, para que seu redutor deve mesclá -lo com o estado do cliente existente corretamente. Mais sobre isso na separação de estados de servidor e cliente.
Embora você possa embrulhar páginas individuais (e não enrolar as pages/_app ), não é recomendado, consulte a seção Última parágrafo na seção 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 ; Lembre -se de que req e res podem não estar disponíveis se getInitialProps for chamado no lado do cliente.
O componente de função sem estado também pode ser substituído pela classe:
class Page extends Component {
public static getInitialProps = wrapper . getInitialPageProps ( store => ( ) => ( { ... } ) ) ;
render ( ) {
// stuff
}
}
export default Page ; Embora você possa embrulhar páginas individuais (e não enrolar as pages/_app ), não é recomendado, consulte a seção Última parágrafo na seção de uso.
pages/_app . Mas esse modo não é compatível com o recurso de exportação estática parcial do Next.JS 9, consulte a explicação abaixo.
O invólucro também pode ser conectado ao seu componente _app (localizado em /pages ). Todos os outros componentes podem usar a função connect of 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 ) ;Em seguida, todas as páginas podem ser simplesmente conectadas (o exemplo considera os componentes da 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 ou getStaticProps no nível da página Você também pode usar getServerSideProps ou getStaticProps no nível da página, neste caso, a ação HYDRATE será despachada duas vezes: com estado após App.getInitialProps e depois com o estado após getServerSideProps ou getStaticProps :
getServerSideProps no nível da página, store no getServerSideProps será executado após App.getInitialProps e terá o estado dele, portanto, o segundo HYDRATE terá estado completo de ambosgetStaticProps no nível da página, store no getStaticProps será executado no horário de compilação e não terá estado do App.getInitialProps porque eles são executados em diferentes contextos e o estado não poderá ser compartilhado. As ações primeiro HYDRATE indicam após App.getInitialProps e o segundo terão estado após getStaticProps (mesmo que tenha sido executado no início do tempo). A maneira mais simples de garantir a fusão adequada é soltar os valores iniciais da 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 ;
}
} ; Suponha que a página despache apenas a ação PAGE e o aplicativo apenas APP , isso torna a fusão do estado segura.
Mais sobre isso na separação de estado de servidor e cliente.
Usando next-redux-wrapper ("The Wrapper"), as seguintes coisas acontecem em uma solicitação:
Fase 1: getInitialProps / getStaticProps / getServerSideProps
makeStore ) com um estado inicial vazio. Ao fazer isso, também fornece os objetos Request e Response como opções para makeStore .getInitialProps do _app e passa a loja criada anteriormente.getInitialProps do _app , junto com o estado da loja.getXXXProps da página e passa a loja criada anteriormente.getXXXProps da página, juntamente com o estado da loja.Fase 2: SSR
makeStoreHYDRATE com o estado da loja anterior como payload_app ou page .Fase 3: Cliente
HYDRATE com o estado da Fase 1 como payload_app ou page .Nota: o estado do cliente não é persistido entre as solicitações (ou seja, a Fase 1 sempre começa com um estado vazio). Portanto, é redefinido nas recarregamentos da página. Considere usar o Redux persiste se você deseja persistir no estado entre solicitações.
Desde a versão 7.0 o suporte de primeira classe do @reduxjs/toolkit foi adicionado.
Exemplo 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 ] ; Recomenda -se exportar State digitado e 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 as páginas que possuem getStaticProps ou getServerSideProps são abertas pelo usuário, a ação HYDRATE será despachada. A payload dessa ação conterá o state no momento da geração estática ou da renderização do lado do servidor, para que seu redutor deve mesclá -lo com o estado do cliente existente corretamente.
A maneira mais fácil e estável de garantir que nada seja acidentalmente substituído é garantir que seu redutor aplique as ações do lado do cliente e do servidor a diferentes subestados do seu estado e que nunca se chocam:
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 ;
}
} ; Se você preferir uma abordagem isomórfica para algumas partes (de preferência pequenas) do seu estado, poderá compartilhá-las entre o cliente e o servidor nas páginas renderizadas pelo servidor usando o Next-Redux-Cookie-Wrapper, uma extensão para o Next-Redux-Wrapper. Nesse caso, para subestados selecionados, o servidor está ciente do estado do cliente (a menos que no getStaticProps ) e não há necessidade de separar o estado do servidor e do cliente.
Além disso, você pode usar uma biblioteca como https://github.com/benjamine/jsondiffpatch para analisar o DIFF e aplicá -lo corretamente.
Eu não recomendo usar withRedux nas pages/_document.js , o Next.js não fornece uma maneira confiável de determinar a sequência quando os componentes serão renderizados. Portanto, a recomendação do próximo.js é melhor ter apenas coisas agnósticas de dados nas pages/_document .
As páginas de erro também podem ser embrulhadas da mesma maneira que as outras páginas.
A transição para uma página de erro (modelo pages/_error.js ) fará com que pages/_app.js sejam aplicadas, mas é sempre uma transição de página inteira (não o HTML5 PushState), para que o cliente tenha a loja criada do zero usando o estado do servidor. Portanto, a menos que você persista a loja no cliente, de alguma forma, o estado do cliente anterior resultante será ignorado.
Você pode usar https://github.com/reduxjs/redux-thunk para despachar ações assíncronas:
function someAsyncAction ( id ) {
return async function ( dispatch , getState ) {
return someApiCall ( id ) . then ( res => {
dispatch ( {
type : 'FOO' ,
payload : res ,
} ) ;
} ) ;
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ;Você também pode instalar https://github.com/pburtchaell/redux-promise-middleware para despachar promessas como ações assíncronas. Siga o guia de instalação da biblioteca, então você poderá lidar com isso assim:
function someAsyncAction ( ) {
return {
type : 'FOO' ,
payload : new Promise ( resolve => resolve ( 'foo' ) ) ,
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ; Se você estiver armazenando tipos complexos, como imutable.js ou JSON Objects em seu estado, um manipulador de serialize e desserialize personalizado pode ser útil para serializar o estado Redux no servidor e desesteriá -lo novamente no cliente. Para fazer isso, forneça as opções de configuração serializeState e deserializeState como configuração do withRedux .
O motivo é que o instantâneo do estado é transferido pela rede do servidor para o cliente como um objeto simples.
Exemplo de uma serialização personalizada de um estado imutável.js usando json-immutable :
const { serialize , deserialize } = require ( 'json-immutable' ) ;
createWrapper ( {
serializeState : state => serialize ( state ) ,
deserializeState : state => deserialize ( state ) ,
} ) ;A mesma coisa usando imutable.js:
const { fromJS } = require ( 'immutable' ) ;
createWrapper ( {
serializeState : state => state . toJS ( ) ,
deserializeState : state => fromJS ( state ) ,
} ) ; [Observe que esse método pode ser inseguro - certifique -se de pensar muito em lidar com sagas assíncronas corretamente. As condições de corrida acontecem com muita facilidade se você não tiver cuidado.] Para utilizar a saga Redux, basta fazer algumas alterações na função makeStore . Especificamente, redux-saga precisa ser inicializado dentro dessa função, em vez de fora dela. (Eu fiz isso no começo e recebi um erro desagradável Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware ). Aqui está como alguém faz exatamente isso. Isso é apenas ligeiramente modificado a partir do exemplo de configuração no início dos documentos. Lembre-se de que essa configuração optará por fora da otimização estática automática: https://err.sh/next.js/opt-out-auto-tatic-timization.
Crie sua saga raiz como de costume e implemente o criador da loja:
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 Então, nas pages/_app aguarde a saga de parada e aguarde o término quando a execução estiver no 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 ou getStaticProps Para usá -lo com getServerSideProps ou getStaticProps você precisa await sagas no manipulador 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 Se você não deseja desativar a pré-renderização automática em seu aplicativo Next.js, poderá gerenciar sagas chamadas de servidor por página, como o exemplo oficial do próximo.js "With Redux Saga". Se você optar por esta opção, aguarde toda e qualquer sagas dentro de qualquer método da Página Next.js. Se você perder em uma das páginas, acabará com o estado inconsistente sendo enviado ao cliente. Portanto, consideramos esperar em _app ser automaticamente mais seguro, mas obviamente a principal desvantagem está optando por não exportações estáticas automáticas.
Se você só precisar persistir pequenas partes do seu estado, o Next-Redux-Cookie-Wrapper pode ser uma alternativa fácil ao Redux persiste que suporta SSR.
Boilerplate: https://github.com/fazlulkarimweb/with-next-redux-wrapper-redux-persist
Honestamente, acho que colocar um portão de persistência não é necessário porque o servidor já pode enviar algum HTML com algum estado, por isso é melhor mostrá -lo imediatamente e, em seguida, aguarde a REHYDRATE a ação para mostrar um delta adicional proveniente do armazenamento de persistência. É por isso que usamos a renderização do lado do servidor em primeiro lugar.
Mas, para aqueles que realmente querem bloquear a interface do usuário enquanto a reidratação está acontecendo, aqui está a solução (ainda assim 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 ,
} ) ; E então, na página Next.js _app , você pode usar o acesso de contexto nua para obter a loja (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 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 >
) ;
} ) ;E então na 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 >
) ) ; A assinatura do createWrapper mudou: em vez de createWrapper<State> você deve usar createWrapper<Store<State>> , todos os tipos serão inferidos automaticamente na Store .
GetServerSidePropsContext e GetStaticPropsContext não são mais exportados do next-redux-wrapper , você deve usar GetServerSideProps , GetServerSidePropsContext , GetStaticProps e GetStaticPropsContext diretamente do next .
Todas as assinaturas como ({store, req, res, ...}) => { ... } foram alteradas para store => ({req, res, ...}) => { ... } para manter a próxima.js internos livres de modificações e para melhor suporte para típicas.
Na versão 7.x você precisa envolver manualmente todos getInitialProps com wrappers adequados: wrapper.getInitialPageProps e wrapper.getInitialAppProps .
Window.Next_Redux_Wrapper_Store foi removido, pois estava causando problemas com a recarga a quente
Grande mudança na maneira como as coisas estão envolvidas na versão 6.
A exportação padrão withRedux está marcada depreciada, você deve criar um wrapper const wrapper = createWrapper(makeStore, {debug: true}) e, em seguida, use wrapper.withRedux(MyApp) .
Sua função makeStore não recebe mais initialState , ela só recebe o contexto: makeStore(context: Context) . O contexto pode ser NextPageContext ou AppContext ou getStaticProps ou getServerSideProps , dependendo da função do ciclo de vida que você envolverá. Em vez disso, você precisa lidar com a ação HYDRATE no redutor. A payload dessa ação conterá o state no momento da geração estática ou da renderização do lado do servidor, para que seu redutor deve mesclá -lo com o estado do cliente existente corretamente.
App não deve mais envolver seus filhos com Provider , agora é feito internamente.
isServer não é passado no context / props , use sua própria função ou verificação simples const isServer = typeof window === 'undefined' ou !!context.req ou !!context.ctx.req .
store não é passada para adereços de componentes embrulhados.
WrappedAppProps foi renomeado para WrapperProps .
Se o seu projeto estava usando o próximo.js 5 e o próximo Redux Wrapper 1.x, essas instruções o ajudarão a atualizar para 2.x.
Upgrade Next.js and wrapper
$ npm install next@6 --save-dev
$ npm install next-redux-wrapper@latest --save Substitua todos os usos de import withRedux from "next-redux-wrapper"; e withRedux(...)(WrappedComponent) em todas as suas páginas com react react Redux connect Hoc:
import { connect } from "react-redux" ;
export default connect ( ... ) ( WrappedComponent ) ;Você também pode ter que reformar sua configuração baseada em objetos do Wrapper para configuração simples do Redux.
Crie o arquivo pages/_app.js com o seguinte 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 a próxima.js 6 Instruções de atualização para todos os seus componentes ( props.router em vez de props.url e assim por diante)
É isso. Seu projeto agora deve funcionar da mesma forma que antes.