next為了給 fetch來的資料提供優化 SEO解決的方案,提供了
getStaticProps
(Static Generation): Fetch data at build time.getStaticPaths
(Static Generation): Specify dynamic routes to pre-render based on data.getServerSideProps
(Server-side Rendering): Fetch data on each request.
當然有時候狀態管理較複雜時,會需要使用 redux,由於最近寫 react使用的都是 redux-saga,這次就試著在 next中使用 redux-saga
yarn add redux react-redux redux-saga redux-devtools-extension
next-redux-wrapperyarn add -D @types/react-redux
原本 getStaticProps等方法是在 NextPage內取得資料後以props的方式向元件內傳遞資料,如果在next中使用redux的話,可以用 useSelector向 store取得資料後進行渲染
我已經預先在同一個專案底下寫了個簡單的 API,只要使用 get方法請求 /apitest/test
就能取得簡單的測試資料
[{ id: 0, name: 'xx'}, .....]
Action
// redux/actions/testAction.ts
import { Action } from 'redux'export interface TestContent {
[key: string]: string
}export interface TestAction extends Action {
type: string
payload?: Array<TestContent>
}export const GET_TESTS_BEGIN = 'GET_TESTS_BEGIN'
export const getTestsBegin = (): {
type: string
} => ({
type: GET_TESTS_BEGIN
})export const GET_TESTS_SUCCESS = 'GET_TESTS_SUCCESS'
export const getTestsSuccess = (tests: Array<TestContent>): TestAction => ({
type: GET_TESTS_SUCCESS,
payload: tests
})
Reducer
// redux/reducers/testReducer.ts
import { HYDRATE } from 'next-redux-wrapper'
import {
FAILURE,
GET_TESTS_SUCCESS,
TestAction,
TestContent
} from 'redux/actions/testAction'export interface RootStateType {
tests?: Array<TestContent> | null
}const initialState: RootStateType = {
tests: null
}const testReducer = (
state = initialState,
action: TestAction
): RootStateType => {
switch (action.type) {
case HYDRATE:
return { ...state, ...action.payload }case GET_TESTS_SUCCESS:
return {
...state,
...{ tests: action.payload }
}default:
return state
}
}export default testReducer
這裡有兩點不同的
- 由於 next-redux-wrapper會派發特殊的 Action:HYDRATE,要記得處理
- 資料型態為陣列或物件的初始值需為 null
第2點我實在是很疑惑,但如果使用[]做初始值,就會看到網頁直接在那邊無限轉圈圈
如果想要使用 redux中的 combineReducers時需要把 HYDRATE抽到 root層級
import { HYDRATE } from 'next-redux-wrapper'
import { combineReducers, Action } from 'redux'
import testReducer, { TestStateType } from './testReducer'
import { TestContent } from '../actions/testAction'export interface RootStateType {
tests: TestStateType | null
}
interface RootAction extends Action {
type: string
payload?: TestContent[] | undefined
}
const initialRootState: RootStateType = {
tests: null
}const rootReducer = (
state = initialRootState,
action: RootAction
): RootStateType => {
if (action.type === HYDRATE) {
return { ...state, ...action.payload }
}return combineReducers({ tests: testReducer })(state, action)
}export default rootReducer
但是初始值需要是 null這點實在是很大的困擾,之後可能要想方法找出問題在哪……
Sagas
// redux/sagas.ts
import { all, AllEffect, ForkEffect, put, takeEvery } from 'redux-saga/effects'
import axios from 'axios'import { getTestsSuccess, GET_TESTS_BEGIN } from './actions/testAction'function* getSelfTests() {
try {
const res = yield axios.get('http://localhost:3000/apitest/test')
yield put(getTestsSuccess(res.data))
} catch (err) {
...
}
}function* testSaga(): Iterator<ForkEffect<never>> {
yield takeEvery(GET_TESTS_BEGIN, getSelfTests)
}function* rootSaga(): Iterator<
AllEffect<Iterator<ForkEffect<never>, any, undefined>>
> {
yield all([testSaga()])
}export default rootSaga
Store
// redux/store.ts
import { applyMiddleware, createStore } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { createWrapper, MakeStore } from 'next-redux-wrapper'
import { composeWithDevTools } from 'redux-devtools-extension'import rootReducer from './reducers/testReducer'
import rootSaga from './sagas'export const makeStore: MakeStore = () => {
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware))
)store.sagaTask = sagaMiddleware.run(rootSaga)return store
}export const wrapper = createWrapper(makeStore, { debug: true })
這裡跟 react中不同,react是直接 return store給 Provider使用,而這裡是包成一個 HOC
另外就是
store.sagaTask = sagaMiddleware.run(rootSaga)
// Property 'sagaTask' does not exist on type 'Store<RootStateType, TestAction> & { dispatch: unknown; }'.
如果不做處理的話 ts會跳出警告
這時可以定義sagaTask來解決
// redux/redux.d.ts
import { Task } from 'redux-saga'declare module 'redux' {
export interface Store {
sagaTask: Task
}
}
最後把 _app.tsx以 wrapper包裹起來
// pages/_app.tsx
import { FC, ReactNode } from 'react'import { wrapper } from 'redux/store'
import '../styles/globals.css'
import '../styles/test.scss'interface MyAppProps {
Component: FC
pageProps: JSX.IntrinsicAttributes & { children?: ReactNode }
}const MyApp = ({ Component, pageProps }: MyAppProps) => (
<Component {...pageProps} />
)export default wrapper.withRedux(MyApp)
最後只要在任意的 NextPage裡使用 useSelector就可以了
import { GetServerSideProps, NextPage } from 'next'
import { useSelector } from 'react-redux'import { RootStateType } from 'redux/reducers/testReducer'
import { wrapper } from 'redux/store'
import { END } from 'redux-saga'
import { getTestsBegin } from 'redux/actions/testAction'
import SideMenu from '../components/SideMenu'
...export const getServerSideProps: GetServerSideProps = wrapper.getServerSideProps(
async ({ store }) => {
if (!store.getState().tests) {
store.dispatch(getTestsBegin())
store.dispatch(END)
}await store.sagaTask.toPromise()
}
)const Home: NextPage = () => {
const tests = useSelector((state: RootStateType) => state.tests)return (
<BaseLayout title='Home'>
<div className='container home-page'>
<div className='row'>
<div className='col-lg-3'>
<SideMenu sideTitle={tests} />
</div>
...
除了不使用 Provider而是自己包成 wrapper,以及使用時還需要呼叫一次 wrapper外,整體來說跟 react中的使用並沒有很大差別,當然也是可以在 useEffect裡面呼叫 useSelector,不過那樣就不能享受 next帶來的 SEO的優化了。
參考: