Next.JS - Access `localStorage` before rendering page - node.js

Let's say I have a user's account information stored in localStorage (client side). I need my Next.JS app to render the webpage's navbar based on what's stored in localStorage (login or logout button). How can I first obtain the value from the client and then render the page? Or perhaps that isn't even what Next.JS is meant to do?

You can do something like this:
Use a variable in the state to prevent the page from being rendered
Use componentDidMount to load data from localStorage
When data is loaded, setState to allow component to be rendered.
It's a react issue, not a next.js issue.
You could use Conditional rendering for step 1.
Also read up on state here, and lastly componentDidMount.
Update:
Nowadays, I would opt for a React hooks implementation instead, but the idea still stands. useEffect can largely accomplish this with some nuances in some situations.
I also realize that there are some possible caveats with NextJS and SSR logic specifically, so this response may not be sufficient. In such cases, I would also look into some other responses below.

As mentioned at https://stackoverflow.com/a/54819843/895245 I haven't been able to truly get localStorage before the first render, only show a fallback page until that happens.
The fundamental issue is that Next.js maps one URL to one pre-render. And React hydration requires the initial server HTML to match the JavaScript structure:
React expects that the rendered content is identical between the server and the client. It can patch up differences in text content, but you should treat mismatches as bugs and fix them. In development mode, React warns about mismatches during hydration. There are no guarantees that attribute differences will be patched up in case of mismatches. This is important for performance reasons because in most apps, mismatches are rare, and so validating all markup would be prohibitively expensive.
That quote is not very clear if text-only changes work or not but the minimal test below shows that it raises a warning in that case, so you don't want to use it.
Therefore the only sure-fire way it to use useEffect to update the page afterwards.
However, when I've tested, the correct render with localStorage shows up so quickly that the intermediate one it is not noticeable at all, I'm not sure it even happens. The only problem is if you make different API calls on each case, see section "Differentiate between "not logged in" and "haven't decided yet" to avoid doing extra API calls" below for an example of that.
What I would like to do is to give a slightly more concrete idea about what was mentioned in that answer.
SWR example
Here is a complete runnable example where the navbar shows login status: https://github.com/cirosantilli/node-express-sequelize-nextjs-realworld-example-app That repository is a fork of this one, both of which are Next.js implementations of the awesome realworld project.
The fallback in that case is just the signed out view of the blog pages, which already contain the key information users are likely to want to see, and can be cached e.g. with ISR.
That demo uses SWR to make the code slightly simpler. The key parts are:
navbar code
login code
The key parts of the code there are:
navbar:
import useSWR from "swr";
const Navbar = () => {
const { data: currentUser } = useSWR("user", key => {
const value = localStorage.getItem(key);
return !!value ? JSON.parse(value) : undefined;
});
login:
import { mutate } from "swr";
const LoginForm = () => {
const handleSubmit = async (e) => {
// Get `user` data structure from API.
mutate("user", data?.user);
We see that when the user logins, we call mutate on the "user" global identifier.
This redraws all components that contain that hook, which includes the navbar, as it setup the hook with the useSWR call.
This way, login first redraws the navbar, and then redirects you to home, so that the home page will have the redrawn navbar immediately. Without mutate, only the page body would redraw, not the navbar.
With this setup:
if you put a console.log(currentUser) just below useSWR, you see that it gets called twice.
So what happens is that it first returns immediately with a cached value (undefined) and the first render starts.
It then starts an async call to the cache, and when that returns, the hook triggers a re-render of the component, and the print happens again with the current user value.
This only happens on initial hydration during refresh/first hit. During internal page changes, there is just a single render.
All of this happens so fast that I can't see the page draw without hte local storage at all, not even with the Chromium debugger timeline frame inspection.
if we add a 2 second delay to the localStorage getter however:
const { data: currentUser } = useSWR("user", async (key) => {
await new Promise(resolve => setTimeout(resolve, 2000))
const value = localStorage.getItem(key);
return !!value ? JSON.parse(value) : undefined;
});
we do observe an intermediate page state with the user logged out, so it could in theory happen.
How it would look like without SWR
Of course, we wouldn't need to use SWR to achieve this.
The SWR documentation gives us the rationale of how thing would look like without SWR https://swr.vercel.app/getting-started to motivate their library.
You would either need to move the state up to a common parent of the login form + navbar, or you could use Context.
function Page () {
const [user, setUser] = useState(null)
// fetch data
useEffect(() => {
const value = localStorage.getItem(key);
const user = !!value ? JSON.parse(value) : undefined;
setUser(user)
}, [])
// global loading state
if (!user) return <Spinner/>
return <div>
<Navbar user={user} />
<Content user={user} />
</div>
}
// child components
function Navbar ({ user }) {
return <div>
...
<Avatar user={user} />
</div>
}
function Content ({ user }) {
return <h1>Welcome back, {user.name}</h1>
}
function Avatar ({ user }) {
return <img src={user.avatar} alt={user.name} />
}
As mentioned at What is difference between lifecycle method and useEffect hook? useEffect is the hook analogue to componentDidMount.
Checking typeof localStorage === 'undefined' leads to a warning
React doesn't like that and warns with something like:
Expected server HTML to contain a matching"
as it notices the difference between hydrated and non-hydrated pages: React 16: Warning: Expected server HTML to contain a matching <div> in <body>
Tested on Next.js 10.2.2.
Minimal reproducible example
Just to play with and see exactly what happens:
pages/index.js
import Link from 'next/link'
import React from 'react'
export default function IndexPage() {
console.error('IndexPage');
let [n, setN] = React.useState(0)
if (typeof localStorage === 'undefined') {
n = '0'
} else {
n = parseInt(localStorage.getItem('n') || '0', 10)
}
return <>
<Link href="/notindex">notindex</Link>
<div
onClick={() => {
localStorage.setItem('n', n + 1)
setN(n + 1)
}}
>increment</div>
<div
onClick={() => {
localStorage.removeItem('n')
setN(0)
}}
>reset</div>
<div>{n}</div>
</>
}
pages/notindex.js
import Link from 'next/link'
export default function NotIndexPage() {
return <Link href="/">index</Link>
}
package.json
{
"name": "test",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "12.0.7",
"react": "17.0.2",
"react-dom": "17.0.2"
}
}
Run:
npm install
npm run dev
Now, if you:
open /
increment
refresh the page
react gives a warning because it notices that the 0 text was changed to 1:
Warning: Text content did not match. Server: "0" Client: "1"
If we click the internal links however to notindex and back, we don't see the warning. This is because hydration is only done on the initial page refresh, further changes are done in Js only.
What we have to do instead is something like this:
import Link from 'next/link'
import React from 'react'
export default function IndexPage() {
console.error('IndexPage');
let [n, setN] = React.useState(0)
React.useEffect(() => {
console.error('useEffect');
setN(parseInt(localStorage.getItem('n') || '0', 10))
}, [])
return <>
<Link href="/notindex">notindex</Link>
<div
onClick={() => {
setN(n + 1)
localStorage.setItem('n', n + 1)
}}
>increment</div>
<div
onClick={() => {
localStorage.removeItem('n')
setN(0)
}}
>reset</div>
<div>{n}</div>
</>
}
Differentiate between "not logged in" and "haven't decided yet" to avoid doing extra API calls
OK, I had another issue: I was making unnecessary API calls, because first the page thought the user was logged out, and then it thought it was logged in, and each of those needed to do different API calls.
Unlike starting to render the wrong page, this would actually have server load consequences, so it was not acceptable.
The solution I used was to differentiate between:
undefined: haven't decided
null: not logged-in
and not make any requests on undefined.
Here's a non-minimized demo:
https://github.com/cirosantilli/node-express-sequelize-nextjs-realworld-example-app/blob/2bbce5199d3a7efa19a3a58426bea25a1cd37579/front/ArticleList.tsx#L33
https://github.com/cirosantilli/node-express-sequelize-nextjs-realworld-example-app/blob/2bbce5199d3a7efa19a3a58426bea25a1cd37579/front/useLoggedInUser.ts
I'll try to minimize it later on.
Another solution: just do SSR
In general, SSR is way simpler than ISR, because you don't have to worry about this get page/ask for data/get data/update page dance from Hell.
ISR is an optimization, and you should only use if there's a proven performance benefit.
Remember that SSR in Next.js is also very data efficient, as Next.js returns only the .json from getServerSideProps on page switches, basically exactly like an API would.
You can then just do authentication from getServerSideProps with cookies, and return the correct page straightaway.

This is how I did it.
const setSession = (accessToken) => {
if (typeof window !== 'undefined')
localStorage.setItem('accessToken', accessToken);
};
const getAccessToken = () => {
if (typeof window !== 'undefined')
return localStorage.getItem('accessToken');
};
Here is where I call them to handle login and to get the access token:
const loginWithEmailAndPassword = async (email, password) => {
const { data } = await axios.post(`${apiUrl}/login`, { email, password });
const { user, accessToken } = data;
if (user) {
setSession(accessToken);
return user;
}
};
const accessToken = getAccessToken();

local storage is not available on the server, there are two options to resolve this
1: create HOC or custom hook to check if the local storage has the data (this is normal react way)
2: you can use cookies to store data on client and server side , which can be then be used getServerSideProps to extract the data and and you can then use this data to display the information accordingly on the initial render.

you can use useEffect hook and useState, so that when component loads, useEffect will fire last, extract data from localStorage and assign it to a STATE from useState.
then you can access your data from useState, states. if that makes sense.
Bottom line, useEffect allows to easily extract data from localStorage, so then you can do what you like with it.
const [userData, setUserData] = useState({});
console.log(userData);
useEffect(()=> {
setUserData(localStorage.getItem('userSession'));
}, [])

The first render which happen on server side can not have access to localStorage and throw the error. To prevent this, add an extra layer of defense with
if (typeof window !== 'undefined') {
// run logic that read/write localStorage
}
Then it should skip the logic when on server and run it when loaded on client side

Related

Best way to implement notification alerts in Node/Express app?

I have inherited a Node/Express code base and my task is to implement a notification alert in the navigation menu. The app database has a table of 'pending accounts', and the alert needs to expose the number of these pending accounts. When an account is 'approved' or 'denied', this notification alert needs to update and reflect the new total of pending accounts.
I know how to do the styling and html here, my question is how best to instantiate, maintain and pass a global dynamic variable that reflects the number of pending accounts, and how to get this variable exposed in the header view which contains the navbar where the notification is to be displayed.
This project a pretty standard Node/Express app, however it uses the view engine Pug. At the root of the view hierarchy is a layout.pug file, which loads most of the scripts and stylesheets, and this layout view in turn loads the header Pug view. When this header view loads, and every time it loads, I need this updated 'pending accounts count' value available to insert into the header view. This is what I am at a bit of a loss on how to go about.
Below is the layout.pug markup with the inclusion of the header pug view. Everything else in the project is pretty straightforward vanilla Node/Express I believe, but I am not very experienced with this stack so if any other code is needed please don't hesitate to ask and I will post. Thanks.
doctype html
html(lang="en")
head
meta(charset='utf-8')
meta(http-equiv='X-UA-Compatible', content='IE=edge')
meta(name='viewport', content='width=device-width, initial-scale=1.0, shrink-to-fit=no')
meta(name='theme-color', content='#4DA5F4')
meta(name='csrf-token', content=_csrf)
script(src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js")
block head
body
include partials/header
I tried including a script in my header.pug view, which contains the navbar element that I want to append the notification too...
link(href='/css/header/header.css' rel='stylesheet')
script(src='/js/registration/registrationsTable.js')
...
Which would run the following function on DOM load....
function getNumberOfPendingRegistrations() {
axios({
method: 'get',
url: '/admin/getNumberOfPendingRegistrations'
}).then((response) => {
if (response.status === 200) {
const { numPendingUsers } = response.data;
console.log(response.data);
} else {
console.log(response.status);
}
})
.catch((error) => {
console.error(error);
});
}
(() => {
document.addEventListener('DOMContentLoaded', () => {
getNumberOfPendingRegistrations();
which would then call the following express function....
exports.getNumberOfPendingRegistrations = (req, res) => {
RegisteredUser.find({ status: 'PENDING' }, (err, allPendingUsers) => {
if (!err) {
let numPendingUsers = 0;
allPendingUsers.forEach(() => {
numPendingUsers++;
});
return res.send(200, { numPendingUsers });
}
console.log('error');
throw err;
});
};
which would then return numPendingUsers to the axios then() function and make that variable available to the header.pug view here....
li.nav-item
a.nav-link(href='/admin/registeredUsers')
span.fa-solid.fa-pen-to-square(style="font-size: 0.7em;")
span(style="margin-left: 0.1em;") Registrations
span.notification=numPendingUsers
NumPendingUsers returns correctly to the axios .then() promise, but is somehow never made available in the header.pug view. It is always undefined. I'm not sure if its a timing issue w when the DOM is loaded, or if I'm making the variable available in .then() incorrecly or what. And also I feel like there must be a simpler way to accomplish all of this.
Figured out that I simply needed to implement a middleware to pass this data to every route. Duh.

ReactJs throwing maximum update depth error

I am building a react app to learn react. I have implemented context to store information about the user when they login and also am making a copy in the localstorage. I verify that users are logged in like this in the App component
function App() {
const [user, setUser] = useContext(UserContext);
return (
<div className="App">
<Router>
<NavBar />
<Switch>
<Route path="/login" exact>
{!user.loggedIn ? <LoginForm /> : <Redirect to="/profile" />}
</Route>
</Switch>
</Router>
</div>
);
And this works fairly well. But if the token has expired; I want the user to be forced into the login page. Which means I need to check the expiration time on every route request as users can be in there profile and if they click on another link they will be redirected to login component. How should I approach this?
I have tried to to use useEffect in app component that sets the state loggedIn to true true when it meets the condition and made a comparison like
useEffect(function () {
const ct = parseInt(Date.now().toString(), 10);
const iat = parseInt(localStorage.getItem('iat'), 10);
if (ct - iat < 60000) {
setUser({ loggedIn: true });
} else {
setUser({ loggedIn: false });
}
});
From my understanding this should happen everytime App component is rendered hence I provide no input to useEffect but this obviously throws a maximum update depth reached error.
How should I implement this behaviour?
Edit:
App component won't rerender because it's never changing and only the subcomponents are. What you can do is, you can put history.location as your dependency in the useEffect, which will trigger the useEffect whenever the path changes. See: How to listen to route changes in react router v4?
If you console.log in your component body, you'll see that it rerenders a lot of times with every node updating. You need to add either an empty dependency array or some dependencies so it only runs the useEffect when it's needed.
useEffect(() => {
//your logic here
}, [])
I think you can also mitigate this error by having another check on your setUser method. This way you'll mitigate a lot of unnecessary calls to the setState method which is one of the reasons for this maximum depth reached error
useEffect(function () {
//other code
if (!loggedIn && ct - iat < 60000) {
setUser({ loggedIn: true });
} else if(loggedIn){
setUser({ loggedIn: false });
}
});
Furthermore, there are usually better ways to check whether the user needs to reauthenticate, if you are using Axios, you can set up a check in the interceptor to do this comparison before making a call and if the user is unauthed, redirect them to login page.
Use useLocation from 'react-router-dom'. Then let location = useLocation() and useEffect(function(){//do stuff}, [location]). This way, we will run the useEffect when the url changes.

how will one angular component tell another to refresh without manually refreshing whole page

I have a node backend with firebase auth that connects to an angular frontend. I have two component that is the navbar and the register form, when I register I call a backend function to register in firebase and sent back the firebase.auth().currentUser, how should my angular frontend navbar detects the arrival of such user and updates itself accordingly, without me refresh the page?
I assume this is how your components are structured:
// In app.component.html
<app-navbar></app-navbar>
<router-outlet></router-outlet> // For rest of the pages.
Here, there are 2 ways (I'll not talk about including ngModel) by which we can pass data to navbar for updating the status/data of navbar.
1. Using Input/Output decorators.
// Your navbar.component.ts
// I'm assuming you have set up model for User. If no, then change the type to **any**.
#Input() currentUser: IUser;
// Your navbar.component.html
<p *ngIf="currentUser"> {{currentUser | json}} </p> // Render this if currentUser exists. By default, it is undefined.
Now, in your App component:
//In your app.component.ts
User: any | IUser;
public authenticateUser() {
this.someServiceToAuthenticateUser.subscribe(
res => this.User = res,
error => console.error(error)
);
}
// In your app.component.html
<app-navbar [currentUser]="User"></app-navbar>
<router-outlet></router-outlet>
Using a common service.
You can create the common service which stores the authenticated user in it. From your App component, you can set the user on the success of authenticating the response and from other components, you can fetch the authenticated user.
export class UserService {
private currentUser: IUser | any;
setCurrentUser(userToSet: any) {
this.currentUser = userToSet;
}
getCurrentUser() {
return this.currentUser;
}
Now, in your App component, if the request for authenticating the use is a success, then set the currentUser of UserService:
//In your app.component.ts
User: any | IUser;
public authenticateUser() {
this.someServiceToAuthenticateUser.subscribe(
res => this.UserService.setCurrentUser(res),
error => console.error(error)
);
}
Now, in your navbar component, get the current user from UserService.
// In navbar.component.ts
currentUser: any;
constructor(private userServicce: UserService) {
getuserFromservice();
}
public getuserFromservice() {
setInterval(() => {
curretnUser = this.userService.getCurrentUser()
}, 3000);
Important: In this approach, notice in navbar component, I have set the current user in setInterval. This is because your getuserFromservice() will run only once (when it is created/rendered for the first time) and at this time, userService will have no currentUser. So navbar will never get the currenUser.
If you want to replace the setInterval approach (I highly encourage to replace it), then use Subjects/BehaviourSubject/ReplaySubject. I did not include them here because Rxjs is a huge topic in itself and including it would be too much to understand here.
Input & Output operator is good but It can't provide flexibility of sharing data like observable
Here You need to use Subject Observable to share data among the app. It's very easy and useful thing. You can send signal to all the lisner when user is registered.
What you need to do
1) Create one service file to share data
- service have subject observable. Using that you can subscribe the channel for user data
2) In the your header component listener that service and set the listener coming data to header user data
3) From your register, Profile update,or anywhere else user data was update, just past that data to subject observable it will automatically update user in entire app. follow below example to achieve this
You can find the working demo in below link
https://jasonwatmore.com/post/2019/02/07/angular-7-communicating-between-components-with-observable-subject

Vue.js authentication after manually entering URL

In my routes array in main.js, I have set
meta : { requiresAuth: true }
for every component except on the UserLogin page.
Regarding navigation resolution, I have set a check before each route as follows:
router.beforeEach((to, from, next) => {
const currentUser = firebase.auth().currentUser;
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
const currentUserState = !!currentUser;
console.table({ currentUserState, requiresAuth });
if (requiresAuth && !currentUser) {
next('/login');
} else {
next();
}
}
With this setup, everything almost works as expected/intended. Logged in users can go anywhere, and anonymous users are redirected to localhost/login.
However, this checking and redirecting only functions when users are navigating by clicking the links in the web app (which are all <router-link>s that go :to="{ name: 'SomeComponent' }".
When any user (registered or anonymous) attempts to navigate their way around the app by typing or copy pasting urls, the app will automatically send them to the login page.
From the console.table output, I can see that manually entering urls always results in currentUserState being false.
It does not remain false: a user that has logged in (currentUserState === true), who then manually navigates somewhere (currentUserState === false), has their currentUserState revert back to true
I can only vaguely guess that the problem is related to rendering and firebase.auth().
firebase.initializeApp(config)
// you need to let firebase try and get the auth state before instantiating the
// the Vue component, otherwise the router will be created and you haven't
// resolved whether or not you have a user
firebase.auth().onAuthStateChanged(function(user) {
app = new Vue({
el: '#app',
template: '<App/>',
components: { App },
router
})
});
I have found a solution to this bug.
In my Vuex store.js, a snapshot of the state is stored into localStorage. This snapshot is updated any time there is a change to the store itself, so the localStorage backup is always up-to-date with the actual state of the Vue.js application itself.
store.subscribe((mutation, state) => {
localStorage.setItem('store', JSON.stringify(state));
});
This store.subscribe is set in my store.js just before the export line.
A function called initStore is also defined in the store as follows
initStore(state) {
if (localStorage.getItem('store')) {
this.replaceState(Object.assign(state, JSON.parse(localStorage.getItem('store'))));
}
},
and after my store object, it is immediately called with
store.commit('initStore').

React server side rendering with backend requests

I have an React app with server side rendering via Express. I have simple App component:
import React, { Component } from 'react';
export default class App extends Component {
constructor(props) {
super(props)
this.state = {
data: []
};
fetch('http://backend', {
mode: 'cors',
})
.then(res => res.json())
.then(data => this.state.data);
}
render() {
return (
<ul>
{this.state.data.map(item => (
<li key={item}>{item}</li>
))}
</ul>
);
}
};
The problem (or more probable feature) is that fetch works async. So browser gets page with empty data.
Is there a way to get from server page with loaded data? Let's say I want to get from server page with loaded posts or something else.
Or do I something wrong at all?
You are not setting the state correctly. Use setState() to set the new state as you cannot update the state without using setState() method: https://facebook.github.io/react/docs/react-component.html#setstate
fetch('http://backend', {
mode: 'cors',
})
.then(res => res.json())
.then(data => this.setState({data: data}));
Also, the code you have added is not server-side rendering using React. You use the method ReactDOMServer.renderToString() to do server-side rendering: https://facebook.github.io/react/docs/react-dom-server.html#rendertostring
Yes, even if you fix the setState() problem you will still be rendering the component before the fetch. The problem is indeed that fetch is asynchronous.
To be able to server-side render back-end calls you need to wait until all backend calls have been made, set the props & state for all components and then render them out.
Two libraries that will help you with figuring out when things are done are Redial (https://github.com/markdalgleish/redial) and redux-promise-counter (https://github.com/bitgenics/redux-promise-counter) if you are using Redux. More on that below.
The next problem you want to solve is getting that data to the client and initialize your components from that, so you don't have to redo (all) the request(s) on the client again.
You could do all that manually, but if you are serious about SSR, you should probably go for something like Redux. A way to store all your application state in one store. Makes it easier to store results, ship it to the client and initialize your store from JSON.

Resources