Reputation: 690
The problem I'm having is hydrating the user data from local storage on app reload/page refresh.
In my project I am using NextJS for frontend, and for support libraries I am using redux-toolkit
for redux management across the application and next-redux-wrapper
for state hydration for the wrapped pages.
The user can log in, and in that case I store the isLoggedIn
boolean in local storage and in the redux state. Depending on the isLoggedIn
boolean value I change the Navbar
component styles (the Navbar
is included directly in _app.tsx
).
When the user refreshes any page the isLoggedIn
boolean is not loaded into the state but is present in local storage.
In the past I have been using redux-persist
but I have opted out of using it because the PersistGate
was blocking the UI from rendering until the persisted data is fetched from storage which conflicts with the idea of SSR.
Currently I have the isLoggedIn
loading problem fixed by using the App.getInitialProps
method in _app.ts
which then results in hydration from next-redux-persist
being called for each and every page loaded, but this introduces another problem: all pages are now server side rendered and there is no NextJS' static page optimisation.
Is there any way to not lose static page optimisation from NextJS, not use the redux-persist
library and still be able to hydrate the client side store when any page is refreshed?
Current code structure (some code is omitted for simplicity):
file: _app.tsx
import { wrapper } from 'store';
const MyApp = ({ Component, pageProps }: AppProps) => {
return (
<>
<Navbar />
<Component {...pageProps} />
</>
);
};
MyApp.getInitialProps = async (appContext) => {
const appProps = await App.getInitialProps(appContext);
return { ...appProps };
};
export default wrapper.withRedux(MyApp);
file: store.ts
import {
combineReducers,
configureStore,
EnhancedStore,
getDefaultMiddleware
} from '@reduxjs/toolkit';
import { createWrapper, MakeStore } from 'next-redux-wrapper';
import userReducer from 'lib/slices/userSlice';
const rootReducer = combineReducers({
user: userReducer
});
const setupStore = (context): EnhancedStore => {
const middleware = [...getDefaultMiddleware(), thunkMiddleware];
if (process.env.NODE_ENV === 'development') {
middleware.push(logger);
}
return configureStore({
reducer: rootReducer,
middleware,
// preloadedState,
devTools: process.env.NODE_ENV === 'development'
});
};
const makeStore: MakeStore = (context) => setupStore(context);
export const wrapper = createWrapper(makeStore, {
debug: process.env.NODE_ENV === 'development'
});
file: userSlice.ts
import { createSlice } from '@reduxjs/toolkit';
import { HYDRATE } from 'next-redux-wrapper';
const initialState = {
isLoggedIn: false
}
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
login: (state) => {
state.isLoggedIn = true;
localStorage.setItem('loggedInData', { isLoggedIn: true });
}
},
extraReducers: (builder) => {
builder
.addCase(HYDRATE, (state, action: any) => {
if (typeof window !== 'undefined') {
const storedLoggedInData = localStorage.getItem('loggedInData');
if (storedLoggedInData != null && storedLoggedInData) {
const parsedJson = JSON.parse(storedLoggedInData);
state.isLoggedIn = parsedJson.isLoggedIn ?? false;
} else {
state.isLoggedIn = false
}
}
});
}
});
export const isLoggedInSelector = (state: RootState) => state.user.isLoggedIn;
export default userSlice.reducer;
file: Navbar.tsx
import { useSelector } from 'react-redux';
import { isLoggedInSelector } from 'lib/slices/userSlice';
export default function Navbar() {
const isLoggedIn = useSelector(isLoggedInSelector);
return (
<div className={`${ isLoggedIn ? 'logged-in-style' : 'logged-out-style'}`}>...</div>
)
}
Upvotes: 10
Views: 20347
Reputation: 1784
Also a note, next-redux-wrapper uses getInitialProps on the app.js/ts when you wrap it with your store. So when you use this library and wrap your app with it you will automatically lose static optimization.
Upvotes: 0
Reputation: 1769
Had the same issue today. The problem is that we need to decouple the client storage from the page rendering and move it into useEffect
where the component is mounted. The basic idea is, you first render the page fully and then update the page using the clients storage information.
Directly incorporating the clients local storage can/will interfere with the hydration.
Here is a code sample that i use
export default const MenuBar = () => {
const isLoggedIn = useSelector((state) => state.isLoggedIn);
useEffect(() => {
// loads from clients local storage
const auth = loadAuthenticationToken();
if (auth !== null) {
// updates the store with local storage data
dispatch(actions.jwtTokenUpdate(auth));
}
}, [dispatch]);
if (isLoggedIn) {
return <p>you are logged in</p>;
} else {
return <p>please log in</p>;
}
}
For reference, a github issue on NextJS: https://github.com/vercel/next.js/discussions/17443
And a blog post where window access for rendering is required: https://dev.to/adrien/creating-a-custom-react-hook-to-get-the-window-s-dimensions-in-next-js-135k
Upvotes: 3
Reputation: 3123
It is mentioned in the doc that if you use getInitialProps in _app.js, you will lose the static optimization. I don't know why you are using redux on the server side, personally I will advise you to only use it on the client side and you won't need to use next-redux-wrapper anymore because it uses getInitialProps under the hood.
The example with redux-toolkit
Upvotes: 1