HOC, который объединяет.
Содержание:
Настройка Redux для статических приложений довольно проста: должен быть создан один магазин Redux, который предоставляется для всех страниц.
Однако при включении статического генератора сайтов или рендеринга на стороне сервера, все начинает усложняться, так как на сервере требуется другой экземпляр магазина, чтобы отобразить компоненты, связанные с Redux.
Кроме того, доступ к Store Redux также может потребоваться во время страницы getInitialProps .
Именно здесь пригодится next-redux-wrapper : он автоматически создает экземпляры магазина для вас и гарантирует, что у них все одно и то же состояние.
Более того, он позволяет правильно обрабатывать сложные случаи, такие как App.getInitialProps (при использовании pages/_app ) вместе с getStaticProps или getServerSideProps на индивидуальном уровне страницы.
Библиотека предоставляет единый интерфейс, независимо от того, какой метод жизненного цикла Next.js вы хотели бы использовать Store .
В примере Next.js https://github.com/vercel/next.js/blob/canary/examples/with-redux-thunk/store.js#l23 хранилище заменяется на навигации. Redux будет повторно обозначить компоненты даже с запоминающими селекторами ( createSelector от recompose ), если заменяется store : https://codesandbox.io/s/redux-store-change-kzs8q, что может повлиять на производительность приложения, вызвав огромный повторный рендеринг всего, даже то, что не изменилось. Эта библиотека гарантирует, что store остается прежним.
npm install next-redux-wrapper react-redux --save Обратите внимание, что next-redux-wrapper требует react-redux в качестве зависимости от сверстников.
Живой пример: https://codesandbox.io/s/next-redux-wrapper-demo-7n2t5.
Все примеры написаны в TypeScript. Если вы используете простой JavaScript, просто пропустите объявления типа. Эти примеры используют Vanilla Redux, если вы используете Redux Toolkit, пожалуйста, обратитесь к специальному примеру.
Next.js имеет несколько механизмов извлечения данных, эта библиотека может прикрепить к любому из них. Но сначала вы должны написать какой -нибудь общий код.
Обратите внимание, что у вашего редуктора должен быть обработчик действий HYDRATE . Обработчик действий HYDRATE должен должным образом согласовать гидратированное состояние поверх существующего состояния (если есть). Такое поведение было добавлено в версии 6 этой библиотеки. Позже поговорим об этом специальном действии.
Создайте файл с именем store.ts :
// store.ts
import { createStore , AnyAction , Store } from 'redux' ;
import { createWrapper , Context , HYDRATE } from 'next-redux-wrapper' ;
export interface State {
tick : string ;
}
// create your reducer
const reducer = ( state : State = { tick : 'init' } , action : AnyAction ) => {
switch ( action . type ) {
case HYDRATE :
// Attention! This will overwrite client state! Real apps should use proper reconciliation.
return { ... state , ... action . payload } ;
case 'TICK' :
return { ... state , tick : action . payload } ;
default :
return state ;
}
} ;
// create a makeStore function
const makeStore = ( context : Context ) => createStore ( reducer ) ;
// export an assembled wrapper
export const wrapper = createWrapper < Store < State > > ( makeStore , { debug : true } ) ; // store.js
import { createStore } from 'redux' ;
import { createWrapper , HYDRATE } from 'next-redux-wrapper' ;
// create your reducer
const reducer = ( state = { tick : 'init' } , action ) => {
switch ( action . type ) {
case HYDRATE :
return { ... state , ... action . payload } ;
case 'TICK' :
return { ... state , tick : action . payload } ;
default :
return state ;
}
} ;
// create a makeStore function
const makeStore = context => createStore ( reducer ) ;
// export an assembled wrapper
export const wrapper = createWrapper ( makeStore , { debug : true } ) ; wrapper.useWrappedStore Настоятельно рекомендуется использовать pages/_app для обертывания всех страниц одновременно, в противном случае из -за потенциальных условий гонки, которые вы можете получить Cannot update component while rendering another component :
import React , { FC } from 'react' ;
import { Provider } from 'react-redux' ;
import { AppProps } from 'next/app' ;
import { wrapper } from '../components/store' ;
const MyApp : FC < AppProps > = ( { Component , ... rest } ) => {
const { store , props } = wrapper . useWrappedStore ( rest ) ;
return (
< Provider store = { store } >
< Component { ... props . pageProps } />
</ Provider >
) ;
} ; Вместо wrapper.useWrappedStore вы также можете использовать Legacy HOC, который может работать с классовыми компонентами.
getInitialProps при использовании class MyApp extends App , которое будет подхвачено оберткой, поэтому вы не должны расширять App , так как вы будете отказаны от автоматической статической оптимизации: https://err.sh/next.js/opt-out-auto-static-optimization. Просто экспортируйте обычный функциональный компонент, как в примере выше.
import React from 'react' ;
import { wrapper } from '../components/store' ;
import { AppProps } from 'next/app' ;
class MyApp extends React . Component < AppProps > {
render ( ) {
const { Component , pageProps } = this . props ;
return < Component { ... pageProps } /> ;
}
}
export default wrapper . withRedux ( MyApp ) ; Каждый раз, когда пользователь открывает страницы, на которых есть getStaticProps или getServerSideProps будет отправлено HYDRATE действие. Это может произойти во время начальной загрузки страницы и во время обычной навигации страницы. payload этого действия будет содержать state в момент статической генерации или рендеринга на стороне сервера, поэтому ваш восстановитель должен правильно объединить его с существующим состоянии клиента.
Самый простой способ - использовать отделение сервера и состояния клиента.
Другой способ - использовать https://github.com/benjamine/jsondiffpatch для анализа Diff и применить его правильно:
import { HYDRATE } from 'next-redux-wrapper' ;
// create your reducer
const reducer = ( state = { tick : 'init' } , action ) => {
switch ( action . type ) {
case HYDRATE :
const stateDiff = diff ( state , action . payload ) as any ;
const wasBumpedOnClient = stateDiff ?. page ?. [ 0 ] ?. endsWith ( 'X' ) ; // or any other criteria
return {
... state ,
... action . payload ,
page : wasBumpedOnClient ? state . page : action . payload . page , // keep existing state or use hydrated
} ;
case 'TICK' :
return { ... state , tick : action . payload } ;
default :
return state ;
}
} ;Или как это (из примера с примером с Redux-wrapper в 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 ) ;
}
} ; Функция createWrapper принимает makeStore в качестве своего первого аргумента. Функция makeStore должна возвращать новый экземпляр Redux Store каждый раз, когда она называется. Здесь нет заметок, это автоматически выполняется внутри обертки.
createWrapper также при желании принимает объект конфигурации в качестве второго параметра:
debug (необязательно, логическое): включить журнал отладкиserializeState и deserializeState : пользовательские функции для сериализации и десериализации состояния Redux см. Пользовательскую сериализацию и десериализацию. Когда makeStore вызывается, он обеспечивается контекстом Next.js, который может быть NextPageContext или AppContext или getStaticProps или контекст getServerSideProps в зависимости от того, какую функцию жизненного цикла вы будете завернуть.
Некоторые из этих контекстов ( getServerSideProps всегда, и NextPageContext , AppContext Иногда, если страница отображается на сервере) может иметь свойства, связанные с запросом и ответом:
req ( IncomingMessage )res ( ServerResponse ) Хотя в makeStore можно создать специальную логику сервера или клиента, я настоятельно рекомендую, чтобы они не имели другого поведения. Это может вызвать ошибки и несоответствия контрольной суммы, которые, в свою очередь, разрушат всю цель рендеринга сервера.
В этом разделе описывается, как прикрепить к функции жизненного цикла GetStaticProps.
Давайте создадим страницу на pages/pageName.tsx :
import React from 'react' ;
import { NextPage } from 'next' ;
import { useSelector } from 'react-redux' ;
import { wrapper , State } from '../store' ;
export const getStaticProps = wrapper . getStaticProps ( store => ( { preview } ) => {
console . log ( '2. Page.getStaticProps uses the store to dispatch things' ) ;
store . dispatch ( {
type : 'TICK' ,
payload : 'was set in other page ' + preview ,
} ) ;
} ) ;
// you can also use `connect()` instead of hooks
const Page : NextPage = ( ) => {
const { tick } = useSelector < State , State > ( state => state ) ;
return < div > { tick } < / div>;
} ;
export default Page ; import React from 'react' ;
import { useSelector } from 'react-redux' ;
import { wrapper } from '../store' ;
export const getStaticProps = wrapper . getStaticProps ( store => ( { preview } ) => {
console . log ( '2. Page.getStaticProps uses the store to dispatch things' ) ;
store . dispatch ( {
type : 'TICK' ,
payload : 'was set in other page ' + preview ,
} ) ;
} ) ;
// you can also use `connect()` instead of hooks
const Page = ( ) => {
const { tick } = useSelector ( state => state ) ;
return < div > { tick } </ div > ;
} ;
export default Page ;getStaticProps открываются пользователем, действие HYDRATE будет отправлено. payload этого действия будет содержать state в момент статической генерации, у него не будет состояния клиента, поэтому ваш восстановитель должен правильно объединить его с существующим государством клиента. Подробнее об этом в разделении сервера и состояния клиента.
Хотя вы можете обернуть отдельные страницы (и не обернуть pages/_app ), это не рекомендуется, см. Последний абзац в разделе использования.
В этом разделе описывается, как прикрепить к функции жизненного цикла GetServersIdeProps.
Давайте создадим страницу на pages/pageName.tsx :
import React from 'react' ;
import { NextPage } from 'next' ;
import { connect } from 'react-redux' ;
import { wrapper , State } from '../store' ;
export const getServerSideProps = wrapper . getServerSideProps ( store => ( { req , res , ... etc } ) => {
console . log ( '2. Page.getServerSideProps uses the store to dispatch things' ) ;
store . dispatch ( { type : 'TICK' , payload : 'was set in other page' } ) ;
} ) ;
// Page itself is not connected to Redux Store, it has to render Provider to allow child components to connect to Redux Store
const Page : NextPage < State > = ( { tick } ) => < div > { tick } < / div>;
// you can also use Redux `useSelector` and other hooks instead of `connect()`
export default connect ( ( state : State ) => state ) ( Page ) ; import React from 'react' ;
import { connect } from 'react-redux' ;
import { wrapper } from '../store' ;
export const getServerSideProps = wrapper . getServerSideProps ( store => ( { req , res , ... etc } ) => {
console . log ( '2. Page.getServerSideProps uses the store to dispatch things' ) ;
store . dispatch ( { type : 'TICK' , payload : 'was set in other page' } ) ;
} ) ;
// Page itself is not connected to Redux Store, it has to render Provider to allow child components to connect to Redux Store
const Page = ( { tick } ) => < div > { tick } </ div > ;
// you can also use Redux `useSelector` and other hooks instead of `connect()`
export default connect ( state => state ) ( Page ) ;getServerSideProps открываются пользователем, действие HYDRATE будет отправлено. payload этого действия будет содержать state в момент рендеринга на стороне сервера, оно не будет иметь клиентского состояния, поэтому ваш редуктор должен правильно объединить его с существующим состоянии клиента должным образом. Подробнее об этом в разделении сервера и состояния клиента.
Хотя вы можете обернуть отдельные страницы (и не обернуть pages/_app ), это не рекомендуется, см. Последний абзац в разделе использования.
Page.getInitialProps import React , { Component } from 'react' ;
import { NextPage } from 'next' ;
import { wrapper , State } from '../store' ;
// you can also use `connect()` instead of hooks
const Page : NextPage = ( ) => {
const { tick } = useSelector < State , State > ( state => state ) ;
return < div > { tick } < / div>;
} ;
Page . getInitialProps = wrapper . getInitialPageProps ( store => ( { pathname , req , res } ) => {
console . log ( '2. Page.getInitialProps uses the store to dispatch things' ) ;
store . dispatch ( {
type : 'TICK' ,
payload : 'was set in error page ' + pathname ,
} ) ;
} ) ;
export default Page ; import React , { Component } from 'react' ;
import { wrapper } from '../store' ;
// you can also use `connect()` instead of hooks
const Page = ( ) => {
const { tick } = useSelector ( state => state ) ;
return < div > { tick } </ div > ;
} ;
Page . getInitialProps = wrapper . getInitialPageProps ( store => ( { pathname , req , res } ) => {
console . log ( '2. Page.getInitialProps uses the store to dispatch things' ) ;
store . dispatch ( {
type : 'TICK' ,
payload : 'was set in error page ' + pathname ,
} ) ;
} ) ;
export default Page ; Имейте в виду, что req и res могут быть недоступны, если getInitialProps вызывается на стороне клиента.
Функциональный компонент без состояния также может быть заменен классом:
class Page extends Component {
public static getInitialProps = wrapper . getInitialPageProps ( store => ( ) => ( { ... } ) ) ;
render ( ) {
// stuff
}
}
export default Page ; Хотя вы можете обернуть отдельные страницы (и не обернуть pages/_app ), это не рекомендуется, см. Последний абзац в разделе использования.
pages/_app . Но этот режим не совместим с функцией Auto Partial Static Export Next.js 9, см. Объяснение ниже.
Обертка также может быть прикреплена к вашему компоненту _app (расположенного в /pages ). Все остальные компоненты могут использовать функцию connect 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 ) ;Тогда все страницы могут быть просто подключены (в примере рассматривается компоненты страницы):
// 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 или getStaticProps на уровне страницы Вы также можете использовать getServerSideProps или getStaticProps на уровне страницы, в этом случае действие HYDRATE будет отправлено дважды: с состоянием после App.getInitialProps , а затем с состоянием после getServerSideProps или getStaticProps :
getServerSideProps на уровне страницы, то store в getServerSideProps будет выполнена после App.getInitialProps и будет иметь состояние от него, так что у второго HYDRATE будет полное состояние от обоихgetStaticProps на уровне страницы, то store в getStaticProps будет выполняться во время компиляции и не будет иметь состояния из App.getInitialProps , поскольку они выполняются в разных контекстах, а состояние не может быть обмен. Первые действия HYDRATE состоятся после App.getInitialProps , а во -вторых, будет состояние после getStaticProps (даже если оно было выполнено ранее во времени). Самый простой способ обеспечить правильное слияние - отбросить начальные значения из 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 ;
}
} ; Предположим, что страница только отправляет действие PAGE и приложение только APP , это обеспечивает безопасным государством.
Подробнее об этом в отделении сервера и состояния клиента.
Используя next-redux-wrapper («Обертка»), следующие вещи случаются по запросу:
Фаза 1: getInitialProps / getStaticProps / getServerSideProps
makeStore ) с пустым начальным состоянием. При этом он также предоставляет объекты Request и Response в качестве опций для makeStore ._app 's getInitialProps и передает ранее созданный магазин._app 's getInitialProps вместе с состоянием магазина.getXXXProps на странице и передает ранее созданный магазин.getXXXProps на странице вместе с состоянием магазина.Фаза 2: SSR
makeStoreHYDRATE с состоянием предыдущего магазина в качестве payload_app или page .Фаза 3: Клиент
HYDRATE с государством с фазы 1 в качестве payload_app или page .Примечание. Состояние клиента не сохраняется по запросам (то есть этап 1 всегда начинается с пустого состояния). Следовательно, он сброшен на перезагрузках страницы. Подумайте о том, чтобы использовать Redux, если вы хотите сохранить состояние между запросами.
Со времени была добавлена 7.0 поддержка @reduxjs/toolkit .
Полный пример: https://github.com/kirill-konshin/next-redux-wrapper/blob/master/packages/demo-redux-toolkit.
import { configureStore , createSlice , ThunkAction } from '@reduxjs/toolkit' ;
import { Action } from 'redux' ;
import { createWrapper , HYDRATE } from 'next-redux-wrapper' ;
export const subjectSlice = createSlice ( {
name : 'subject' ,
initialState : { } as any ,
reducers : {
setEnt ( state , action ) {
return action . payload ;
} ,
} ,
extraReducers : {
[ HYDRATE ] : ( state , action ) => {
console . log ( 'HYDRATE' , state , action . payload ) ;
return {
... state ,
... action . payload . subject ,
} ;
} ,
} ,
} ) ;
const makeStore = ( ) =>
configureStore ( {
reducer : {
[ subjectSlice . name ] : subjectSlice . reducer ,
} ,
devTools : true ,
} ) ;
export type AppStore = ReturnType < typeof makeStore > ;
export type AppState = ReturnType < AppStore [ 'getState' ] > ;
export type AppDispatch = AppStore [ 'dispatch' ] ;
export type AppThunk < ReturnType = void > = ThunkAction < ReturnType , AppState , unknown , Action > ;
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = ( ) => useDispatch < AppDispatch > ( ) ;
export const useAppSelector : TypedUseSelectorHook < RootState > = useSelector ;
export const fetchSubject =
( id : any ) : AppThunk =>
async dispatch => {
const timeoutPromise = ( timeout : number ) => new Promise ( resolve => setTimeout ( resolve , timeout ) ) ;
await timeoutPromise ( 200 ) ;
dispatch (
subjectSlice . actions . setEnt ( {
[ id ] : {
id ,
name : `Subject ${ id } ` ,
} ,
} ) ,
) ;
} ;
export const wrapper = createWrapper < AppStore > ( makeStore ) ;
export const selectSubject = ( id : any ) => ( state : AppState ) => state ?. [ subjectSlice . name ] ?. [ id ] ; Рекомендуется экспортировать типизированное State и ThunkAction :
export type AppStore = ReturnType < typeof makeStore > ;
export type AppState = ReturnType < AppStore [ 'getState' ] > ;
export type AppThunk < ReturnType = void > = ThunkAction < ReturnType , AppState , unknown , Action > ; Каждый раз, когда пользователь открывает страницы, на которых есть getStaticProps или getServerSideProps будет отправлено HYDRATE действие. payload этого действия будет содержать state в момент статической генерации или рендеринга на стороне сервера, поэтому ваш восстановитель должен правильно объединить его с существующим состоянии клиента.
Самый простой и самый стабильный способ убедиться, что ничего не является случайно перезаписано, - это убедиться, что ваш редуктор применяет клиентские и серверные действия к разным субстанциям вашего состояния, и они никогда не сталкиваются:
export interface State {
server : any ;
client : any ;
}
const reducer = ( state : State = { tick : 'init' } , action : AnyAction ) => {
switch ( action . type ) {
case HYDRATE :
return {
... state ,
server : {
... state . server ,
... action . payload . server ,
} ,
} ;
case 'SERVER_ACTION' :
return {
... state ,
server : {
... state . server ,
tick : action . payload ,
} ,
} ;
case 'CLIENT_ACTION' :
return {
... state ,
client : {
... state . client ,
tick : action . payload ,
} ,
} ;
default :
return state ;
}
} ; const reducer = ( state = { tick : 'init' } , action ) => {
switch ( action . type ) {
case HYDRATE :
return {
... state ,
server : {
... state . server ,
... action . payload . server ,
} ,
} ;
case 'SERVER_ACTION' :
return {
... state ,
server : {
... state . server ,
tick : action . payload ,
} ,
} ;
case 'CLIENT_ACTION' :
return {
... state ,
client : {
... state . client ,
tick : action . payload ,
} ,
} ;
default :
return state ;
}
} ; Если вы предпочитаете изоморфный подход для некоторых (предпочтительно небольших) частей вашего состояния, вы можете поделиться ими между клиентом и сервером на страницах с трансляцией сервера, используя следующий RETUX-COOKIE-WRAPPER, расширение на следующий-RED-WRAPPER. В этом случае для выбранных субстанций сервер знает о состоянии клиента (если в getStaticProps ), и нет необходимости отделять состояние сервера и клиента.
Кроме того, вы можете использовать такую библиотеку, как https://github.com/benjamine/jsondiffpatch, чтобы проанализировать DIFF и применять его должным образом.
Я не рекомендую использовать withRedux в pages/_document.js , Next.js не обеспечивает надежного способа определения последовательности при отображении компонентов. Таким образом, в соответствии с рекомендацией next.js лучше иметь просто алкогольные вещи на pages/_document .
Страницы ошибок также могут быть обернуты так же, как и любые другие страницы.
Переход на страницу ошибки (шаблон pages/_error.js ) приведет к применению pages/_app.js , но это всегда полный переход по странице (не HTML5 PushState), поэтому клиент будет создан с нуля с использованием состояния с сервера. Таким образом, если вы не упорствуете в магазине на клиенте как -то, полученное в результате предыдущего государства клиента будет проигнорировано.
Вы можете использовать https://github.com/reduxjs/redux-thunk для отправки Async Actions:
function someAsyncAction ( id ) {
return async function ( dispatch , getState ) {
return someApiCall ( id ) . then ( res => {
dispatch ( {
type : 'FOO' ,
payload : res ,
} ) ;
} ) ;
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ;Вы также можете установить https://github.com/pburtchaell/redux-promise-dlearware, чтобы отправлять обещания как асинхронные действия. Следуйте руководству по установке библиотеки, тогда вы сможете обращаться с ней так:
function someAsyncAction ( ) {
return {
type : 'FOO' ,
payload : new Promise ( resolve => resolve ( 'foo' ) ) ,
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ; Если вы сохраняете сложные типы, такие как Immutable.js или JSON объекты в вашем штате, пользовательский сериализуется и десериализируйте обработчик для сериализации Redux на сервере и повторной десериализации его на клиенте. Для этого предоставьте serializeState и deserializeState в качестве параметров конфигурации для withRedux .
Причина в том, что снимки состояния передается по сети от сервера в клиент в качестве простого объекта.
Пример пользовательской сериализации состояния Immatable.js с использованием json-immutable :
const { serialize , deserialize } = require ( 'json-immutable' ) ;
createWrapper ( {
serializeState : state => serialize ( state ) ,
deserializeState : state => deserialize ( state ) ,
} ) ;То же самое с использованием Immutable.js:
const { fromJS } = require ( 'immutable' ) ;
createWrapper ( {
serializeState : state => state . toJS ( ) ,
deserializeState : state => fromJS ( state ) ,
} ) ; [Обратите внимание, этот метод может быть небезопасным - убедитесь, что вы правильно задумались о обработке асинхронных саг. Условия гонки случаются очень легко, если вы не будете осторожны.] Чтобы использовать сагу Redux, нужно просто внести некоторые изменения в их функцию makeStore . В частности, redux-saga должен быть инициализирован внутри этой функции, а не за ее пределами. (Сначала я сделал это и получил неприятную ошибку, сообщив мне Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware ). Вот как кто -то делает именно это. Это лишь немного изменен из примера настройки в начале документов. Имейте в виду, что эта настройка выберет вас из автоматической статической оптимизации: https://err.sh/next.js/opt-out-auto-static-optimization.
Создайте свою корневую сагу как обычно, а затем реализуйте создателя магазина:
import { createStore , applyMiddleware , Store } from 'redux' ;
import { createWrapper , Context } from 'next-redux-wrapper' ;
import createSagaMiddleware , { Task } from 'redux-saga' ;
import reducer , { State } from './reducer' ;
import rootSaga from './saga' ;
export interface SagaStore extends Store {
sagaTask ?: Task ;
}
export const makeStore = ( context : Context ) => {
// 1: Create the middleware
const sagaMiddleware = createSagaMiddleware ( ) ;
// 2: Add an extra parameter for applying middleware:
const store = createStore ( reducer , applyMiddleware ( sagaMiddleware ) ) ;
// 3: Run your sagas on server
( store as SagaStore ) . sagaTask = sagaMiddleware . run ( rootSaga ) ;
// 4: now return the store:
return store ;
} ;
export const wrapper = createWrapper < Store < State > > ( makeStore , { debug : true } ) ; import { createStore , applyMiddleware } from 'redux' ;
import { createWrapper } from 'next-redux-wrapper' ;
import createSagaMiddleware from 'redux-saga' ;
import reducer from './reducer' ;
import rootSaga from './saga' ;
export const makeStore = context => {
// 1: Create the middleware
const sagaMiddleware = createSagaMiddleware ( ) ;
// 2: Add an extra parameter for applying middleware:
const store = createStore ( reducer , applyMiddleware ( sagaMiddleware ) ) ;
// 3: Run your sagas on server
store . sagaTask = sagaMiddleware . run ( rootSaga ) ;
// 4: now return the store:
return store ;
} ;
export const wrapper = createWrapper ( makeStore , { debug : true } ) ; pages/_app Затем на pages/_app wate Stop Saga и подождите, пока она закончит, когда выполнение будет на сервере:
import React from 'react' ;
import App , { AppInitialProps } from 'next/app' ;
import { END } from 'redux-saga' ;
import { SagaStore , wrapper } from '../components/store' ;
class WrappedApp extends App < AppInitialProps > {
public static getInitialProps = wrapper . getInitialAppProps ( store => async context => {
// 1. Wait for all page actions to dispatch
const pageProps = {
// https://nextjs.org/docs/advanced-features/custom-app#caveats
... ( await App . getInitialProps ( context ) ) . pageProps ,
} ;
// 2. Stop the saga if on server
if ( context . ctx . req ) {
store . dispatch ( END ) ;
await ( store as SagaStore ) . sagaTask . toPromise ( ) ;
}
// 3. Return props
return { pageProps } ;
} ) ;
public render ( ) {
const { Component , pageProps } = this . props ;
return < Component { ... pageProps } /> ;
}
}
export default wrapper . withRedux ( WrappedApp ) ; import React from 'react' ;
import App from 'next/app' ;
import { END } from 'redux-saga' ;
import { SagaStore , wrapper } from '../components/store' ;
class WrappedApp extends App {
static getInitialProps = wrapper . getInitialAppProps ( store => async context => {
// 1. Wait for all page actions to dispatch
const pageProps = {
// https://nextjs.org/docs/advanced-features/custom-app#caveats
... ( await App . getInitialProps ( context ) ) . pageProps ,
} ;
// 2. Stop the saga if on server
if ( context . ctx . req ) {
store . dispatch ( END ) ;
await store . sagaTask . toPromise ( ) ;
}
// 3. Return props
return { pageProps } ;
} ) ;
public render ( ) {
const { Component , pageProps } = this . props ;
return < Component { ... pageProps } /> ;
}
}
export default wrapper . withRedux ( WrappedApp ) ; getServerSideProps или getStaticProps Чтобы использовать его с помощью getServerSideProps или getStaticProps , которые вам нужно await саг в обработке каждой страницы:
export const getServerSideProps = ReduxWrapper . getServerSideProps ( async ( { store , req , res , ... etc } ) => {
// regular stuff
store . dispatch ( ApplicationSlice . actions . updateConfiguration ( ) ) ;
// end the saga
store . dispatch ( END ) ;
await store . sagaTask . toPromise ( ) ;
} ) ; getInitialProps внутри _app Если вы не хотите отказаться от автоматического предварительного предопределения в вашем приложении Next.js, вы можете управлять сагами с сервером на основе на странице, как официальный пример Next.js "с Redux Saga". Если вы выполните эту опцию, убедитесь, что вы ожидаете любых саг в рамках любых методов страницы следующего.js. Если вы пропустите это на одной из страниц, вы получите непоследовательное состояние, которое будет отправлено клиенту. Таким образом, мы рассматриваем ожидание в _app автоматически безопаснее, но, очевидно, основным недостатком является отключение автоматического статического экспорта.
Если вам нужно только сохранить небольшие части вашего штата, следующий редюкс-куки-WRAPPER может быть легкой альтернативой Redux Story, который поддерживает SSR.
CoalerPlate: https://github.com/fazlulkarimweb/with-next-redux-wrapper-redux-persist
Честно говоря, я думаю, что размещение настойчивости не требуется, потому что сервер уже может отправить некоторый HTML с некоторым состоянием, поэтому лучше показать его сразу же, а затем дождаться, когда произойдет действие REHYDRATE , чтобы показать дополнительную дельту, исходящую от хранилища настойчивости. Вот почему мы в первую очередь используем рендеринг на стороне сервера.
Но для тех, кто действительно хочет заблокировать пользовательский интерфейс, пока происходит регидрация, вот решение (все еще хакерское):
// lib/redux.js
import logger from 'redux-logger' ;
import { applyMiddleware , createStore } from 'redux' ;
const SET_CLIENT_STATE = 'SET_CLIENT_STATE' ;
export const reducer = ( state , { type , payload } ) => {
// Usual stuff with HYDRATE handler
if ( type === SET_CLIENT_STATE ) {
return {
... state ,
fromClient : payload ,
} ;
}
return state ;
} ;
const makeConfiguredStore = reducer => createStore ( reducer , undefined , applyMiddleware ( logger ) ) ;
const makeStore = ( ) => {
const isServer = typeof window === 'undefined' ;
if ( isServer ) {
return makeConfiguredStore ( reducer ) ;
} else {
// we need it only on client side
const { persistStore , persistReducer } = require ( 'redux-persist' ) ;
const storage = require ( 'redux-persist/lib/storage' ) . default ;
const persistConfig = {
key : 'nextjs' ,
whitelist : [ 'fromClient' ] , // make sure it does not clash with server keys
storage ,
} ;
const persistedReducer = persistReducer ( persistConfig , reducer ) ;
const store = makeConfiguredStore ( persistedReducer ) ;
store . __persistor = persistStore ( store ) ; // Nasty hack
return store ;
}
} ;
export const wrapper = createWrapper ( makeStore ) ;
export const setClientState = clientState => ( {
type : SET_CLIENT_STATE ,
payload : clientState ,
} ) ; А затем на странице Next.js _app вы можете использовать доступ к контексту, чтобы получить магазин (https://react-redux.js.org/api/provider#props):
// pages/_app.tsx
import React from 'react' ;
import App from 'next/app' ;
import { ReactReduxContext } from 'react-redux' ;
import { wrapper } from './lib/redux' ;
import { PersistGate } from 'redux-persist/integration/react' ;
export default wrapper . withRedux (
class MyApp extends App {
render ( ) {
const { Component , pageProps } = this . props ;
return (
< ReactReduxContext . Consumer >
{ ( { store } ) => (
< PersistGate persistor = { store . __persistor } loading = { < div > Loading </ div > } >
< Component { ... pageProps } />
</ PersistGate >
) }
</ ReactReduxContext . Consumer >
) ;
}
} ,
) ;Или с помощью крючков:
// pages/_app.tsx
import React from 'react' ;
import App from 'next/app' ;
import { useStore } from 'react-redux' ;
import { wrapper } from './lib/redux' ;
import { PersistGate } from 'redux-persist/integration/react' ;
export default wrapper . withRedux ( ( { Component , pageProps } ) => {
const store = useStore ( ) ;
return (
< PersistGate persistor = { store . __persistor } loading = { < div > Loading </ div > } >
< Component { ... pageProps } />
</ PersistGate >
) ;
} ) ;А затем на странице Next.js:
// pages/index.js
import React from 'react' ;
import { connect } from 'react-redux' ;
export default connect ( state => state , { setClientState } ) ( ( { fromServer , fromClient , setClientState } ) => (
< div >
< div > fromServer: { fromServer } </ div >
< div > fromClient: { fromClient } </ div >
< div >
< button onClick = { e => setClientState ( 'bar' ) } > Set Client State </ button >
</ div >
</ div >
) ) ; Подпись createWrapper изменилась: вместо createWrapper<State> вы должны использовать createWrapper<Store<State>> , все типы будут автоматически выведены из Store .
GetServerSidePropsContext и GetStaticPropsContext больше не экспортируются из next-redux-wrapper , вы должны использовать GetServerSideProps , GetServerSidePropsContext , GetStaticProps и GetStaticPropsContext непосредственно с next .
Все подписи, такие как ({store, req, res, ...}) => { ... } были изменены на store => ({req, res, ...}) => { ... } , чтобы сохранить next.js Internals без модификаций и для лучшей поддержки типов.
В версии 7.x вы должны вручную обернуть все getInitialProps с помощью правильных оберток: wrapper.getInitialPageProps и wrapper.getInitialAppProps .
window.next_redux_wrapper_store было удалено, так как это вызывало проблемы с горячей перезагрузкой
Основное изменение в том, как все обернуто в версии 6.
Экспорт по умолчанию withRedux отмечен устарел, вы должны создать оболочку const wrapper = createWrapper(makeStore, {debug: true}) , а затем использовать wrapper.withRedux(MyApp) .
Ваша функция makeStore больше не получает initialState , она получает только контекст: makeStore(context: Context) . Контекст может быть NextPageContext или AppContext или getStaticProps или контекст getServerSideProps в зависимости от того, какую функцию жизненного цикла вы будете завернуть. Вместо этого вам нужно обрабатывать действие HYDRATE в редукторе. payload этого действия будет содержать state в момент статической генерации или рендеринга на стороне сервера, поэтому ваш восстановитель должен правильно объединить его с существующим состоянии клиента.
App больше не должно обернуть своих детей Provider , теперь это делается внутри.
isServer не передается в context / props , используйте свою собственную функцию или простую проверку const isServer = typeof window === 'undefined' или !!context.req или !!context.ctx.req .
store не передается в оберщенные компоненты.
WrappedAppProps были переименованы в WrapperProps .
Если ваш проект использовал next.js 5 и Next Redux Orbper 1.x, эти инструкции помогут вам обновиться до 2.x.
Обновление Next.js и обертка
$ npm install next@6 --save-dev
$ npm install next-redux-wrapper@latest --save Заменить все использование import withRedux from "next-redux-wrapper"; и withRedux(...)(WrappedComponent) на всех ваших страницах с простым React redux connect hoc:
import { connect } from "react-redux" ;
export default connect ( ... ) ( WrappedComponent ) ;Возможно, вам также придется переформатировать конфигурацию на основе объектов на основе обертки на простую конфигурацию React Redux.
Создайте файл pages/_app.js со следующим минимальным кодом:
// pages/_app.js
import React from 'react'
import { Provider } from 'react-redux' ;
import App from 'next/app' ;
import { wrapper } from '../store' ;
class MyApp extends App {
static async getInitialProps = ( context ) => ( {
pageProps : {
// https://nextjs.org/docs/advanced-features/custom-app#caveats
... ( await App . getInitialProps ( context ) ) . pageProps ,
}
} ) ;
render ( ) {
const { Component , pageProps } = this . props ;
return (
< Component { ... pageProps } />
) ;
}
}
export default wrapper . withRedux ( MyApp ) ; props.router следующим props.url
Вот и все. Ваш проект должен теперь работать так же, как и раньше.