مخصص يجمع Next.js و Redux معًا
محتويات:
يعد إنشاء Redux للتطبيقات الثابتة أمرًا بسيطًا إلى حد ما: يجب إنشاء متجر Redux واحد يتم توفيره لجميع الصفحات.
عندما يشارك NEXT.JS STATIT SITE GERLATOR أو عرض جانب الخادم ، ومع ذلك ، تبدأ الأمور في التعقيد مع حاجة إلى مثيل متجر آخر على الخادم لتقديم مكونات متصلة بالذوق.
علاوة على ذلك ، قد تكون هناك حاجة أيضًا للوصول إلى 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 العادي فقط حذف إعلانات النوع. تستخدم هذه الأمثلة Redux الفانيليا ، إذا كنت تستخدم مجموعة أدوات Redux ، فيرجى الرجوع إلى مثال مخصص.
لدى 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 يمكنك أيضًا استخدام Hacacy Hoc ، والتي يمكن أن تعمل مع المكونات القائمة على الفصل.
getInitialProps عام عند استخدام class MyApp extends App الذي سيتم التقاطه بواسطة Wrapper ، لذلك يجب ألا تمتد App كما ستختتم من التحسين الثابت التلقائي: https://err.sh/next.js/opt-out-to-static-ptimization. ما عليك سوى تصدير مكون وظيفي منتظم كما في المثال أعلاه.
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 ;
}
} ;أو مثل هذا (من مثال مع ريدوكس-وورابر في repo next.js):
const reducer = ( state , action ) => {
if ( action . type === HYDRATE ) {
const nextState = {
... state , // use previous state
... action . payload , // apply delta from hydration
} ;
if ( state . count ) nextState . count = state . count ; // preserve count value on client side navigation
return nextState ;
} else {
return combinedReducer ( state , action ) ;
}
} ; تقبل وظيفة 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 أيضًا. ولكن هذا الوضع غير متوافق مع ميزة التصدير الثابت التلقائي لـ 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 ("The Wrapper") ، تحدث الأشياء التالية بناءً على الطلب:
المرحلة 1: getInitialProps / getStaticProps / getServerSideProps
makeStore ) مع حالة أولية فارغة. في القيام بذلك ، يوفر أيضًا كائنات Request Response كخيارات لـ makeStore .getInitialProps لـ _app ويمرر المتجر الذي تم إنشاؤه مسبقًا.getInitialProps الخاصة بـ _app ، إلى جانب حالة المتجر.getXXXProps الخاصة بالصفحة وتجاوز المتجر الذي تم إنشاؤه مسبقًا.getXXXProps الخاصة بالصفحة ، إلى جانب حالة المتجر.المرحلة 2: SSR
makeStoreHYDRATE مع حالة المتجر السابق كحمولة payload_app أو page .المرحلة 3: العميل
HYDRATE مع الحالة من المرحلة الأولى كحمولة payload_app أو page .ملاحظة: لا يتم استمرار حالة العميل عبر الطلبات (مثل المرحلة الأولى تبدأ دائمًا بحالة فارغة). وبالتالي ، يتم إعادة تعيينه على إعادة تحميل الصفحة. فكر في استخدام 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 ;
}
} ; إذا كنت تفضل نهجًا متماثلًا لبعض الأجزاء (التي يفضلها) من ولايتك ، فيمكنك مشاركتها بين العميل والخادم على الصفحات التي تم تقديمها للخادم باستخدام WRED-Cookie-Wrapper ، وهو امتداد إلى Redux-Wrapper التالي. في هذه الحالة ، بالنسبة للبديل المحدد ، يكون الخادم على دراية بحالة العميل (ما لم تكن في getStaticProps ) وليس هناك حاجة لفصل حالة الخادم والعميل.
أيضًا ، يمكنك استخدام مكتبة مثل https://github.com/benjamine/jsondiffpatch لتحليل Diff وتطبيقها بشكل صحيح.
لا أوصي باستخدام withRedux في pages/_document.js ، لا يوفر next.js طريقة موثوقة لتحديد التسلسل عند تقديم المكونات. لذا ، من الأفضل أن يكون هناك فقط أشياء غير مصابة بالبيانات في pages/_document .
يمكن أيضًا لف صفحات الخطأ بنفس طريقة أي صفحات أخرى.
سيؤدي الانتقال إلى صفحة خطأ (قالب pages/_error.js ) إلى تطبيق pages/_app.js ، ولكنها دائمًا ما يكون انتقالًا كاملًا للصفحة (وليس HTML5 PushState) ، لذلك سيقوم العميل بإنشاء المتجر من نقطة الصفر باستخدام الحالة من الخادم. لذلك ما لم تستمر في المتجر على العميل بطريقة ما ، سيتم تجاهل حالة العميل السابقة الناتجة.
يمكنك استخدام https://github.com/reduxjs/redux-thunk لإرسال إجراءات Async:
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-middleware من أجل إرسال الوعود كإجراءات ASYNC. اتبع دليل التثبيت للمكتبة ، ثم ستتمكن من التعامل معها على هذا النحو:
function someAsyncAction ( ) {
return {
type : 'FOO' ,
payload : new Promise ( resolve => resolve ( 'foo' ) ) ,
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ; إذا كنت تقوم بتخزين أنواع معقدة مثل كائنات Immutable.js أو JSON في ولايتك ، فقد يكون المعالج المتصدر المخصص والمعالج مفيدًا لتسلسل حالة Redux على الخادم وإخراجها مرة أخرى على العميل. للقيام بذلك ، توفير serializeState و deserializeState كخيارات التكوين لـ withRedux .
والسبب هو أن لقطة الحالة يتم نقلها عبر الشبكة من الخادم إلى العميل ككائن عادي.
مثال على التسلسل المخصص لحالة غير 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 ) ,
} ) ; [ملاحظة ، قد تكون هذه الطريقة غير آمنة - تأكد من وضع الكثير من التفكير في التعامل مع Sagas Async بشكل صحيح. تحدث ظروف السباق بسهولة شديدة إذا لم تكن حذراً.] لاستخدام Redux Saga ، يتعين على المرء ببساطة إجراء بعض التغييرات على وظيفة 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-ptimization.
قم بإنشاء ملحمة الجذر الخاصة بك كالمعتاد ، ثم قم بتنفيذ منشئ المتجر:
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 انتظر 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 Sagas في معالج كل صفحة:
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 الخاص بك ، فيمكنك إدارة Sagas المدار على الخادم على أساس لكل صفحة مثل مثال Next.js الرسمي "مع Redux Saga". إذا ذهبت مع هذا الخيار ، فيرجى التأكد من أنك تنتظر أي وجميع SAGAs ضمن أساليب صفحة NEXT.JS. إذا فاتتكها على إحدى الصفحات ، فسوف ينتهي بك الأمر مع إرسال حالة غير متناسقة إلى العميل. لذلك ، فإننا نعتبر الانتظار في _app أكثر أمانًا تلقائيًا ، ولكن من الواضح أن العيب الرئيسي هو اختيار الصادرات الثابتة التلقائية.
إذا كنت بحاجة فقط إلى الاستمرار في أجزاء صغيرة من ولايتك ، فقد يكون Redux-Cookie-Wrapper بديلاً سهلاً لـ Redux الذي يدعم SSR.
boilerplate: https://github.com/fazlulkarimweb/with-next-wedux-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, ...}) => { ... } من أجل الحفاظ على exter.js internals خالية من التعديلات ودعم طباعة أفضل.
getInitialProps wrapper.getInitialPageProps 7.x 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 و Redux Wrapper 1.x ستساعدك هذه التعليمات على الترقية إلى 2.x.
ترقية Next.js و wrapper
$ npm install next@6 --save-dev
$ npm install next-redux-wrapper@latest --save استبدل جميع استخدامات import withRedux from "next-redux-wrapper"; و withRedux(...)(WrappedComponent) في جميع صفحاتك مع Redux Redux connect Plain Redux Hoc:
import { connect } from "react-redux" ;
export default connect ( ... ) ( WrappedComponent ) ;قد تضطر أيضًا إلى إعادة تهيئة التكوين المستند إلى كائن Wrapper إلى Config 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 ) ; اتبع إرشادات ترقية Next.js 6 لجميع مكوناتك ( props.router بدلاً من props.url وما إلى ذلك)
هذا كل شيء. يجب أن يعمل مشروعك الآن كما كان من قبل.