Issue
I have two errors only when the app is deployed, not in dev mode nor the local build, which is another issue altogether.
1st error: Text content does not match server-rendered HTML.
2nd error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
I found out that the errors are caused by the formatting of the time that I get from an API in Unix timestamp format (current.dt), but I don't know how else to handle this. Should I format the date on the server i.e. in getServerSideProps? Or are these errors caused by something else?
Edit: Live app with errors: https://weather-test-mu.vercel.app/
WeatherDisplay:
import type { Current, Location } from '../../types/typeWeatherApi';
const WeatherDisplay = ({
location,
current,
}: {
location: Location;
current: Current;
}) => {
// This causes the errors:
const hour = ('0' + new Date(current.dt * 1000).getHours()).slice(-2);
const minutes = ('0' + new Date(current.dt * 1000).getMinutes()).slice(-2);
const currentTime = `${hour}:${minutes}`;
return (
<article>
<h1>{location.name}</h1>
<p>{location.country}</p>
<p>{current.feels_like}</p>
{/* This causes the error: */}
<p>{currentTime}</p>
</article>
);
};
export default WeatherDisplay;
Index page with getServerSideProps:
import axios from 'axios';
import { useState } from 'react';
import type { NextPage, GetServerSideProps } from 'next';
import type { Data, Location } from '../types/typeWeatherApi';
import { getCurrentWeather } from './api/currentWeather';
import { getCurrentLocation } from './api/currentLocation';
import WeatherDisplay from '../components/weather-display/weather-display';
const Home: NextPage = ({ initialWeather, initialLocation }: any) => {
const [location, setLocation] = useState<Location>(initialLocation);
const [weatherData, setWeatherData] = useState<Data>(initialWeather);
const [units, setUnits] = useState('metric');
const [lang, setLang] = useState('en');
const getGeolocationData = () => {
navigator.geolocation.getCurrentPosition(
(position) => {
axios
.get(
`/api/currentLocation?lon=${position.coords.longitude}&lat=${position.coords.latitude}`
)
.then((response) => setLocation(response.data[0]));
},
(error) => {
console.warn(`ERROR(${error.code}): ${error.message}`);
},
{
timeout: 10000,
maximumAge: 0,
}
);
};
const getCurrentWeather = async () => {
await axios
.get(
`/api/currentWeather?lon=${location.lon}&lat=${location.lat}&units=${units}&lang=${lang}`
)
.then((response) => setWeatherData(response.data))
.catch((error) => console.error(error));
};
return (
<>
<section>
<div>
<p>Latitude: {location.lat}</p>
<p>Longitude: {location.lon}</p>
</div>
<button onClick={getGeolocationData}>Get Current Location</button>
<button onClick={getCurrentWeather}>Get Current Weather</button>
</section>
<section className="current-weather">
<WeatherDisplay location={location} current={weatherData.current} />
</section>
</>
);
};
export const getServerSideProps: GetServerSideProps = async () => {
const defaultWeatherQuery = {
lat: '51.5072',
lon: '0.1276',
exclude: '',
units: 'metric',
lang: 'en',
};
const defaultLocationQuery = {
lat: defaultWeatherQuery.lat,
lon: defaultWeatherQuery.lon,
};
const defaultWeather = await getCurrentWeather(defaultWeatherQuery);
const defaultLocation = await getCurrentLocation(defaultLocationQuery);
return {
props: {
initialWeather: defaultWeather,
initialLocation: defaultLocation[0],
},
};
};
export default Home;
Solution
I got an answer from GitHub discussions on next.js, I will paste it here for anyone facing the same problem as me:
Quote icyJoseph:
Hi,
Yeah so this two errors are combined.
Because the client, on the first frame, needs to see the same HTML as the server sent over, in order to place event listeners and place siblings and children correctly, if there's an error while this is being done, React logs an error. This has happened all the way back to React 17 AFAIK.
Problem number 2 kicks in with the new rendering root, which sees this as a rendering error, which is unfriendly to concurrent features, so hydration fails and it throws the entire thing out the window.
At least that's how I interpret the second error. Lots of people just ignored error number 1, during the entire 2 years React 17 was out, not saying you did, but many use libraries that did, and others just ignored them. Possible work around
Time and randomness are two of the things that most commonly produce this.
I would attack this problem like this:
- Let the server render the time, but in UTC format
- Optionally, put CSS that makes this hidden, but still present on the layout (not display none)
- From the client, update to the correct time zone, also make the time visible
import { CSSProperties, useEffect, useState } from "react"; import { Example } from "../components/Example"; const TimeDisplay = ({ time }: { time: number }) => { const [currentTime, setCurrentTime] = useState(() => { const hour = ("0" + new Date(time * 1000).getUTCHours()).slice(-2); const minutes = ("0" + new Date(time * 1000).getUTCMinutes()).slice(-2); return `${hour}:${minutes} UTC`; }); useEffect(() => { setCurrentTime(() => { const hour = ("0" + new Date(time * 1000).getHours()).slice(-2); const minutes = ("0" + new Date(time * 1000).getMinutes()).slice(-2); return `${hour}:${minutes}`; }); }, [time]); // optionally make this content take space, but remain invisible, to avoid layout shifts // it's better to use a CSS class instead const style: CSSProperties = { visibility: currentTime.includes("UTC") ? "hidden" : "visible", }; return ( <article> <p style={style}>{currentTime}</p> </article> ); };
I'd recommend also mixing in the DateTimeFormat API, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat, which can take care of showing the time zone for you:
const date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0, 200)); options = { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short' }; console.log(new Intl.DateTimeFormat('sv-SE', options).format(date));
If I run the above on a repl, I get 3:00:00 AM UTC (this would be what your server sends), but if I run it on the browsers I get, 4:00:00 CET.
The source code of you page contains the time seen by the server.
Since users arrive to you from different time zones, you have to account for a frame where you show UTC, and then you show the time for the users time zone. Combining this with CSS visibility is good, because you can avoid layout shifts and such, the time will take its place, but it won't be visible.
Robots, crawlers still see the time, and for users with JS disabled, you could add a tag that makes the date visible again.
Answered By - Karel
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.