In React, Redux is commonly used for global state management, often integrated using middleware like Redux Saga or Thunk. When transitioning to Next.js, Redux works similarly, but you’ll need to consider how Next.js handles server-side rendering (SSR) and static site generation (SSG). This guide compares Redux implementation in React and Next.js and explores how to handle SSR and persistent stores in Next.js.
Redux Store in React
In a typical React app, Redux is integrated using the following steps:
- Create a Redux store: This is often done in a
store.js
or store/index.js
file.
- Provider Setup: Redux Provider is wrapped around your application in
index.js
or App.js
.
- Connecting Components: Components use
useSelector
and useDispatch
hooks to connect to Redux.
Example (React)
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
export default store;
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
Redux Store in NextJS
In Next.js, you will handle Redux slightly differently to accommodate server-side rendering (SSR) and static site generation (SSG).
-
Create the Redux store: This can be done similarly to React.
-
Next.js Integration: You'll need to integrate the store using next-redux-wrapper
to enable SSR.
-
Provider Setup: You’ll wrap your _app.js
component with Redux Provider using next-redux-wrapper.
Example (Next.js)
Example (Next.js)
npm install next-redux-wrapper redux redux-saga
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { createWrapper } from 'next-redux-wrapper';
import rootReducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const makeStore = () => {
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
return store;
};
export const wrapper = createWrapper(makeStore);
import { wrapper } from '../store';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default wrapper.withRedux(MyApp);
Comparing Redux Implementation
1. Types
In both React and Next.js, you define action types the same way. Typically, these are stored in a types.js
file.
export const FETCH_DATA = 'FETCH_DATA';
export const FETCH_SUCCESS = 'FETCH_SUCCESS';
export const FETCH_ERROR = 'FETCH_ERROR';
2. Actions
In both cases, actions are functions that return a type and payload. In Next.js, actions work identically to React.
import { FETCH_DATA, FETCH_SUCCESS, FETCH_ERROR } from './types';
export const fetchData = () => ({ type: FETCH_DATA });
export const fetchSuccess = (data) => ({ type: FETCH_SUCCESS, payload: data });
export const fetchError = (error) => ({ type: FETCH_ERROR, payload: error });
3. Reducers
Reducers in React and Next.js work the same way. The key difference in Next.js is how the initial state may vary based on SSR and SSG needs.
import { FETCH_DATA, FETCH_SUCCESS, FETCH_ERROR } from './types';
const initialState = {
data: null,
loading: false,
error: null,
};
export default function rootReducer(state = initialState, action) {
switch (action.type) {
case FETCH_DATA:
return { ...state, loading: true };
case FETCH_SUCCESS:
return { ...state, data: action.payload, loading: false };
case FETCH_ERROR:
return { ...state, error: action.payload, loading: false };
default:
return state;
}
}
4. Sagas
Redux Sagas handle side effects like API calls. In Next.js, the next-redux-wrapper
ensures sagas work with SSR.
import { takeLatest, call, put } from 'redux-saga/effects';
import { FETCH_DATA, fetchSuccess, fetchError } from './actions';
import { fetchApi } from './api';
function* fetchDataSaga() {
try {
const data = yield call(fetchApi);
yield put(fetchSuccess(data));
} catch (error) {
yield put(fetchError(error.message));
}
}
export default function* rootSaga() {
yield takeLatest(FETCH_DATA, fetchDataSaga);
}
5. Persistent Store
In React, you may use libraries like redux-persist to persist the Redux store. In Next.js, persistence can be slightly more complex due to SSR.
React Persistent Store:
npm install redux-persist
import { createStore } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import rootReducer from './reducers';
const persistConfig = {
key: 'root',
storage,
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = createStore(persistedReducer);
const persistor = persistStore(store);
export { store, persistor };
Next.js Persistent Store:
While Redux persist works similarly, with Next.js SSR, you’ll need to be cautious about when you persist the store to avoid server-side issues.
import storage from 'redux-persist/lib/storage';
import { persistReducer, persistStore } from 'redux-persist';
import { createWrapper } from 'next-redux-wrapper';
const persistConfig = {
key: 'root',
storage,
whitelist: ['someReducer'],
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const makeStore = () => {
const store = createStore(persistedReducer);
store.__PERSISTOR = persistStore(store);
return store;
};
export const wrapper = createWrapper(makeStore);
Best Practices
-
SSR with Redux: When using Redux with Next.js, ensure that you're careful about what data is shared across server and client. Avoid persisting sensitive or large data on the server.
-
State Hydration: Use Next.js getServerSideProps
or getStaticProps
to hydrate the store with data when needed.
export const getServerSideProps = wrapper.getServerSideProps((store) => async (context) => {
store.dispatch(fetchData());
await store.sagaTask.toPromise();
return { props: {} };
});
- Lazy Loading: Consider splitting up reducers and sagas to avoid a large bundle size. Use
combineReducers
or lazy loading of reducers where applicable.
Final Thoughts
The main differences between implementing Redux in React and Next.js revolve around SSR and Next.js-specific features. Redux remains consistent in both, but special attention must be paid to handling server-side rendering and hydration in Next.js.
By using next-redux-wrapper, Redux works seamlessly in Next.js, allowing you to manage global state efficiently while benefiting from server-side rendering.