เฉพาะกิจ
สารบัญ:
การตั้งค่า redux สำหรับแอพคงที่ค่อนข้างง่าย: ต้องสร้างร้านค้า Redux เดียวที่มีให้สำหรับทุกหน้า
เมื่อ Next.js สร้างไซต์แบบคงที่หรือการเรนเดอร์ด้านเซิร์ฟเวอร์มีส่วนเกี่ยวข้องอย่างไรก็ตามสิ่งต่าง ๆ เริ่มมีความซับซ้อนเนื่องจากจำเป็นต้องมีอินสแตนซ์ร้านค้าอื่นบนเซิร์ฟเวอร์เพื่อแสดงส่วนประกอบที่เชื่อมต่อ Redux
นอกจากนี้อาจจำเป็นต้องใช้การเข้าถึง Redux Store ในระหว่าง 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 หากคุณใช้จาวาสคริปต์ธรรมดาเพียงแค่ละทิ้งการประกาศประเภท ตัวอย่างเหล่านี้ใช้ Vanilla Redux หากคุณใช้ Redux Toolkit โปรดดูตัวอย่างเฉพาะ
Next.js มีกลไกการดึงข้อมูลหลายอย่างไลบรารีนี้สามารถแนบกับสิ่งใดก็ได้ แต่ก่อนอื่นคุณต้องเขียนรหัสทั่วไป
โปรดทราบว่าตัวลดของคุณ จะต้อง มีตัวจัดการแอ็คชั่น HYDRATE HYDRATE Action Handler จะต้องกระทบยอดสถานะความชุ่มชื้นอย่างถูกต้องที่ด้านบนของสถานะที่มีอยู่ (ถ้ามี) พฤติกรรมนี้ถูกเพิ่มในเวอร์ชัน 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 ซึ่งจะถูกหยิบขึ้นมาโดย wrapper ดังนั้นคุณ ต้องไม่ขยาย App ในขณะที่คุณจะถูกเลือกไม่ได้จากการเพิ่มประสิทธิภาพแบบคงที่อัตโนมัติ: https://err.sh/next.js/opt-out-auto-aptimization เพียงส่งออกองค์ประกอบการทำงานปกติดังตัวอย่างด้านบน
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 เพื่อวิเคราะห์ความแตกต่างและใช้อย่างถูกต้อง:
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 ;
}
} ;หรือเช่นนี้ (จากตัวอย่าง with-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 ยังเลือกที่จะยอมรับวัตถุ config เป็นพารามิเตอร์ที่สอง:
debug (เป็นทางเลือกบูลีน): เปิดใช้งาน DEGUG LOGGINGserializeState และ deserializeState : ฟังก์ชั่นที่กำหนดเองสำหรับการทำให้เป็นอนุกรมและ deserializing สถานะ redux ดูการทำให้เป็นอนุกรมและ deserialization ที่กำหนดเอง เมื่อ makeStore ถูกเรียกใช้จะมีบริบท next.js ซึ่งอาจเป็น NextPageContext หรือ AppContext หรือ getStaticProps หรือ getServerSideProps โดยขึ้นอยู่กับฟังก์ชั่น Lifecycle ที่คุณจะห่อ
บริบทเหล่านั้นบางส่วน ( 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 Action จะถูกส่ง 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 ได้เช่นกัน แต่โหมดนี้ไม่สามารถใช้งานได้กับคุณสมบัติการส่งออกแบบสแตติกบางส่วนของ JS 9 ของ JS 9 ดูคำอธิบายด้านล่าง
Wrapper สามารถแนบกับส่วนประกอบ _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 HYDRATE ถูกดำเนินการหลังจาก App.getInitialProps และจะมีสถานะจากมัน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 เป็นตัวเลือกสำหรับ makeStoregetInitialProps ของ _app และผ่านร้านค้าที่สร้างขึ้นก่อนหน้านี้getInitialProps ของ _app พร้อมกับสถานะของร้านค้าgetXXXProps ของหน้าและผ่านร้านค้าที่สร้างขึ้นก่อนหน้านี้getXXXProps ของหน้าพร้อมกับสถานะของร้านค้าขั้นตอนที่ 2: SSR
makeStoreHYDRATE ด้วยสถานะของร้านค้าก่อนหน้านี้เป็น payload_app หรือ pageขั้นตอนที่ 3: ลูกค้า
HYDRATE ด้วยสถานะจากระยะที่ 1 เป็น payload_app หรือ pageหมายเหตุ: สถานะของลูกค้าจะไม่คงอยู่ในคำขอ (เช่นเฟส 1 เริ่มต้นด้วยสถานะว่างเสมอ) ดังนั้นจึงถูกรีเซ็ตในการโหลดหน้าใหม่ พิจารณาการใช้ Redux Persist หากคุณต้องการคงอยู่ระหว่างการร้องขอ
ตั้งแต่เวอร์ชัน 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 ;
}
} ; หากคุณต้องการวิธีการ isomorphic สำหรับบางส่วน (เล็กกว่า) บางส่วนของสถานะของคุณคุณสามารถแชร์ระหว่างไคลเอนต์และเซิร์ฟเวอร์บนหน้าเซิร์ฟเวอร์ที่แสดงผลโดยใช้ Next-Redux-cookie-wrapper ซึ่งเป็นส่วนขยายไปยังผู้เขียน Next-Redux ในกรณีนี้สำหรับผู้ที่เลือกเซิร์ฟเวอร์จะรับรู้ถึงสถานะของลูกค้า (เว้นแต่ใน getStaticProps ) และไม่จำเป็นต้องแยกเซิร์ฟเวอร์และสถานะไคลเอ็นต์
นอกจากนี้คุณสามารถใช้ไลบรารีเช่น https://github.com/benjamine/jsondiffpatch เพื่อวิเคราะห์ความแตกต่างและใช้อย่างถูกต้อง
ฉันไม่แนะนำให้ใช้ withRedux ใน pages/_document.js , next.js ไม่ได้ให้วิธีที่เชื่อถือได้ในการกำหนดลำดับเมื่อส่วนประกอบจะแสดงผล ดังนั้นตามคำแนะนำของ 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 ( ) ) ; หากคุณกำลังจัดเก็บประเภทที่ซับซ้อนเช่นวัตถุที่ไม่เปลี่ยนรูป JS หรือ JSON ในสถานะของคุณการทำให้เป็นอนุกรมและตัวจัดการ deserialize ที่กำหนดเองอาจมีประโยชน์ในการทำให้สถานะ Redux บนเซิร์ฟเวอร์และ deserialize อีกครั้งบนไคลเอนต์ ในการทำเช่นนั้นให้จัดเตรียม 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 ) ,
} ) ; [หมายเหตุวิธีนี้ อาจ ไม่ปลอดภัย - ตรวจสอบให้แน่ใจว่าคุณได้คิดมากในการจัดการ Async Sagas อย่างถูกต้อง เงื่อนไขการแข่งขันเกิดขึ้นได้ง่ายมากหากคุณไม่ระวัง] เพื่อใช้ 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-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 รอหยุด 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 หากคุณไม่ต้องการยกเลิกการแสดงผลล่วงหน้าโดยอัตโนมัติในแอพถัดไปของคุณคุณสามารถจัดการ Sagas ที่เรียกว่าเซิร์ฟเวอร์ได้ในแต่ละหน้าเช่นตัวอย่างอย่างเป็นทางการ next.js "กับ Redux Saga" หากคุณไปกับตัวเลือกนี้โปรดตรวจสอบให้แน่ใจว่าคุณรอ sagas ใด ๆ และทั้งหมดภายในวิธีการใด ๆ หน้า. js หากคุณพลาดในหนึ่งหน้าคุณจะต้องจบลงด้วยสถานะที่ไม่สอดคล้องกันที่ถูกส่งไปยังลูกค้า ดังนั้นเราจึงพิจารณารอให้ _app ปลอดภัยยิ่งขึ้นโดยอัตโนมัติ แต่เห็นได้ชัดว่าข้อเสียเปรียบหลักคือการยกเลิกการส่งออกแบบคงที่อัตโนมัติ
หากคุณต้องการเพียงแค่ยังคงมีส่วนเล็ก ๆ ในสถานะของคุณเท่านั้นการร่ำรวยแบบเรดลักซ์-คาดูกี้อาจเป็นทางเลือกที่ง่ายสำหรับ Redux ยังคงมีอยู่ที่รองรับ SSR
Boilerplate: https://github.com/fazlulkarimweb/with-next-redux-wrapper-redux-persist
สุจริตฉันคิดว่าการวางเกตการคงอยู่นั้นไม่จำเป็นเพราะเซิร์ฟเวอร์สามารถส่ง HTML บางอย่าง กับสถานะ บางอย่าง ได้ดังนั้นจึงเป็นการดีกว่าที่จะแสดงให้เห็นทันทีจากนั้นรอให้การกระทำของคืน REHYDRATE เกิดขึ้นเพื่อแสดงเดลต้าเพิ่มเติมที่มาจากที่เก็บข้อมูลต่อ นั่นเป็นเหตุผลที่เราใช้การแสดงผลด้านเซิร์ฟเวอร์ตั้งแต่แรก
แต่สำหรับผู้ที่ต้องการปิดกั้น UI ในขณะที่การคืนสภาพกำลังเกิดขึ้นนี่คือวิธีการแก้ปัญหา (ยังคงแฮ็ค):
// 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 ,
} ) ; และในหน้าถัดไป _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 >
) ;
} ) ;และในหน้าถัดไป 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, ...}) => { ... }
ในเวอร์ชัน 7.x คุณต้องห่อ getInitialProps ทั้งหมดด้วยตนเองด้วย wrappers ที่เหมาะสม: wrapper.getInitialPageProps และ wrapper.getInitialAppProps
window.next_redux_wrapper_store ถูกลบออกเนื่องจากเป็นสาเหตุของปัญหาเกี่ยวกับการโหลดซ้ำ
การเปลี่ยนแปลงครั้งใหญ่ในวิธีที่สิ่งต่าง ๆ ถูกห่อหุ้มในเวอร์ชัน 6
การส่งออกเริ่มต้น withRedux ถูกทำเครื่องหมายว่าเลิกใช้แล้วคุณควรสร้าง wrapper 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 และ wrapper redux ถัดไป 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) ในทุกหน้าของคุณด้วย React Redux connect HOC:
import { connect } from "react-redux" ;
export default connect ( ... ) ( WrappedComponent ) ;คุณอาจต้องฟอร์แมตการกำหนดค่าวัตถุที่ใช้ wrapper ของคุณเพื่อกำหนดค่า Redux Simple Redux Config
สร้างไฟล์ 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 และอื่น ๆ )
แค่นั้นแค่นั้น โครงการของคุณควรทำงานเหมือนเดิม