다음에 JS와 Redux를 함께 가져 오는 사태
내용물:
정적 앱 용 Redux 설정은 다소 간단합니다. 모든 페이지에 제공되는 단일 Redux 스토어를 작성해야합니다.
그러나 JS 정적 사이트 생성기 또는 서버 측 렌더링이 관련되면 서버에서 다른 스토어 인스턴스가 필요한 경우 Redux-Connected 구성 요소를 렌더링하기 때문에 상황이 복잡해지기 시작합니다.
또한 페이지의 getInitialProps 에서 Redux Store 에 대한 액세스가 필요할 수도 있습니다.
이곳은 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 스토어가 탐색시 교체 중입니다. https://codesandbox.io/s/redux-store-change-kzs8q를 store 하면 메모 화 된 선택기 ( recompose 에서 createSelector )를 사용하여 구성 요소를 다시 렌더링합니다. 이 라이브러리는 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 툴킷을 사용하는 경우 전용 예제를 참조하십시오.
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 대신 클래스 기반 구성 요소와 함께 작동 할 수있는 레거시 HOC를 사용할 수도 있습니다.
class MyApp extends App 사용할 때 일반적인 getInitialProps 제공하므로 자동 정적 최적화 (https://err.sh/next.js/opt-oun-auto-static-optinization에서 App 확장해서는 안됩니다 . 위의 예에서와 같이 일반 기능 구성 요소를 내보내십시오.
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 포함되므로 Reducer는 기존 클라이언트 상태와 올바르게 병합해야합니다.
가장 간단한 방법은 서버 및 클라이언트 상태 분리를 사용하는 것입니다.
또 다른 방법은 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 ;
}
} ;또는 이와 같이 (다음. JS Repo의 Redux-Wrapper 예제에서) :
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 ; getInitialProps 가 클라이언트 측에서 호출되면 req 및 res 사용할 수 없을 수 있습니다.
무국적 기능 구성 요소도 클래스로 대체 할 수 있습니다.
class Page extends Component {
public static getInitialProps = wrapper . getInitialPageProps ( store => ( ) => ( { ... } ) ) ;
render ( ) {
// stuff
}
}
export default Page ; 개별 페이지를 랩핑 할 수 있지만 ( pages/_app 를 랩핑하지는 않지만) 권장하지는 않지만 사용 섹션의 마지막 단락을 참조하십시오.
pages/_app 에서도 작업을 발송할 수 있습니다. 그러나이 모드는 Next.js 9의 자동 부분 정적 내보내기 기능과 호환되지 않습니다. 아래 설명을 참조하십시오.
래퍼는 _app 구성 요소 ( /pages 에있는)에 부착 할 수도 있습니다. 다른 모든 구성 요소는 react-redux 의 connect 기능을 사용할 수 있습니다.
// 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 App.getInitialProps 수 있습니다.이 getStaticProps HYDRATE 조치 getServerSideProps 두 번 발송됩니다.
getServerSideProps 사용하는 경우 getServerSideProps 에 store App.getInitialProps 에서 상태를 가질 수있게되므로 두 번째 HYDRATE 둘 다의 전체 상태를 갖습니다.getStaticProps 사용하는 경우 getStaticProps 에 store 컴파일 시간에 실행되며 다른 상황에서 실행되기 때문에 App.getInitialProps 에서 상태가 없습니다 . App.getInitialProps 와 두 번째 HYDRATE 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 의 옵션으로 제공합니다._app 의 getInitialProps 함수를 호출하고 이전에 만든 매장을 통과합니다._app 의 getInitialProps 메소드에서 반환 된 소품을 가져옵니다.getXXXProps 함수를 호출하고 이전에 만든 매장을 통과합니다.getXXXProps 메소드에서 반환 된 소품을 가져옵니다.2 단계 : SSR
makeStore 사용하여 새로운 매장을 만듭니다HYDRATE 동작을 payload 로 발송합니다._app 또는 page 구성 요소의 속성으로 전달됩니다.3 단계 : 클라이언트
HYDRATE 동작을 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 포함되므로 Reducer는 기존 클라이언트 상태와 올바르게 병합해야합니다.
실수로 덮어 쓰지 않은지 확인하는 가장 쉽고 안정적인 방법은 Reducer가 클라이언트 측 및 서버 측 작업을 해당 상태의 다른 물질에 적용하고 절대 충돌하지 않도록하는 것입니다.
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 ;
}
} ; 주의 일부 (바람직하게는 작은) 부분에 대해 동형 접근 방식을 선호하는 경우 다음 -Sevux-Cookie-Wrapper를 사용하여 서버 렌더링 된 페이지에서 클라이언트와 서버간에 차세대 래퍼로의 확장을 공유 할 수 있습니다. 이 경우 선택한 변전소의 경우 서버는 클라이언트의 상태를 알고 있으며 ( getStaticProps 에 있지 않은 경우) 서버 및 클라이언트 상태를 분리 할 필요가 없습니다.
또한 https://github.com/benjamine/jsondiffpatch와 같은 라이브러리를 사용하여 Diff를 분석하고 올바르게 적용 할 수 있습니다.
pages/_document.js 에서 withRedux 사용하지 않는 것이 좋습니다. Next.js는 구성 요소가 렌더링 될 때 시퀀스를 결정하는 신뢰할 수있는 방법을 제공하지 않습니다. 따라서 다음 .js 권장 사항 pages/_document 에 데이터 공유 사항 만 갖는 것이 좋습니다.
오류 페이지는 다른 페이지와 같은 방식으로 래핑 할 수 있습니다.
오류 페이지 ( pages/_error.js 템플릿)로 전환하면 pages/_app.js 적용되지만 항상 전체 페이지 전환 (HTML5 PushState가 아님)이므로 클라이언트는 서버에서 상태를 사용하여 처음부터 스토어를 생성 할 수 있습니다. 따라서 고객의 상점을 지속하지 않으면 어떻게 든 이전 클라이언트 상태는 무시됩니다.
https://github.com/reduxjs/redux-thunk를 사용하여 비동기 동작을 발송 할 수 있습니다.
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를 설치할 수도 있습니다. 라이브러리의 설치 안내서를 따르면 다음과 같이 처리 할 수 있습니다.
function someAsyncAction ( ) {
return {
type : 'FOO' ,
payload : new Promise ( resolve => resolve ( 'foo' ) ) ,
} ;
}
// usage
await store . dispatch ( someAsyncAction ( ) ) ; 귀하의 주에 Empable.js 또는 JSON 객체와 같은 복잡한 유형을 저장하는 경우, 사용자 지정 직렬화 및 사형화 핸들러는 서버에서 Redux 상태를 직렬화하고 클라이언트에서 다시 해제하는 데 유용 할 수 있습니다. 이를 위해 withRedux 에 대한 구성 옵션으로 serializeState 및 deserializeState 제공하십시오.
그 이유는 State Snapshot이 서버에서 클라이언트로 일반 객체로 네트워크를 통해 전송되기 때문입니다.
json-immutable 사용하여 Empableable.js 상태의 맞춤형 직렬화 예 :
const { serialize , deserialize } = require ( 'json-immutable' ) ;
createWrapper ( {
serializeState : state => serialize ( state ) ,
deserializeState : state => deserialize ( state ) ,
} ) ;Emutable.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-opticization을 선택하지 않습니다.
평소와 같이 루트 사가를 만들고 상점 제작자를 구현하십시오.
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 대기 중지 사가에서 실행이 서버에있을 때 완료되기를 기다립니다.
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 와 함께 사용하려면 각 페이지의 핸들러에서 Sagas를 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 ( ) ;
} ) ; _app 에 getInitialProps 없는 사용법 Next.js 앱에서 자동 사전 렌더링을 옵트 아웃하지 않으려면 공식 Next.js와 같이 페이지별로 서버 호출 사가를 관리 할 수 있습니다. 이 옵션을 사용하는 경우 Next.js Page 메소드 내에서 모든 Sagas를 기다리고 있는지 확인하십시오. 페이지 중 하나에서 놓친 경우 일관되지 않은 상태가 클라이언트에게 전송됩니다. 따라서 우리는 _app 에서 기다리는 것이 자동으로 더 안전하다고 생각하지만, 주요 단점은 자동 정적 내보내기를 선택하는 것입니다.
주의 작은 부분 만 지속되면 Next-Redux-Cookie-Wrapper는 SSR을 지원하는 Redux 지속에 대한 쉬운 대안 일 수 있습니다.
보일러 플레이트 : 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 ,
} ) ; 그런 다음 Next.js _app 페이지에서 Bare Context 액세스를 사용하여 상점을 얻을 수 있습니다 (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 적절한 포장지로 수동으로 wrapper.getInitialPageProps 해야 wrapper.getInitialAppProps .
window.next_redux_wrapper_store가 핫 재 장전 문제를 일으켜 제거되어 제거되었습니다.
버전 6에서 사물이 어떻게 포장되는지의 주요 변화.
Default Export withRedux 가 감가 상각 된 것으로 표시되며 Wapper const wrapper = createWrapper(makeStore, {debug: true}) 생성 한 다음 wrapper.withRedux(MyApp) 사용해야합니다.
makeStore 기능은 더 이상 initialState 얻지 못하며 makeStore(context: Context) 만받습니다. 컨텍스트는 다음 수명주기 기능에 따라 NextPageContext 또는 AppContext 또는 getStaticProps 또는 getServerSideProps 컨텍스트 일 수 있습니다. 대신, 당신은 환원제에서 HYDRATE 작용을 처리해야합니다. 이 조치의 payload 정적 생성 또는 서버 측 렌더링 순간에 state 포함되므로 Reducer는 기존 클라이언트 상태와 올바르게 병합해야합니다.
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로 업그레이드하는 데 도움이됩니다.
다음에 JS와 래퍼를 업그레이드하십시오
$ npm install next@6 --save-dev
$ npm install next-redux-wrapper@latest --save import withRedux from "next-redux-wrapper"; 그리고 withRedux(...)(WrappedComponent) 모든 페이지에서 일반 반응 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.url props.router 다음.
그게 다야. 프로젝트는 이제 이전과 동일하게 작동해야합니다.