環境
- Next.js 9.4.4
- React 16.13.1
再現手順
Next.jsで、下記のようなコンポーネントを定義します。そしてnext dev
で開発サーバを立ち上げるか、next build && next export
で静的サイト生成して、サイトを表示します。
export default function Home() {
const now = Number(new Date());
return (
<>
<p><a href={now}>{now}</a></p>
</>
)
}
このときのnow
変数で表示される値が、href属性とaタグ内部テキストとで同じ変数なのに異なる値を取ってしまいます。
ふたつの値は、それぞれ下記のものになっています。
- href属性内の値は、サーバサイドでのページ生成時の値(静的サイトのビルド時の値)
- テキストは、クライアントサイドでのページ表示時の値
開発サーバ立ち上げ時はブラウザのコンソールにprop did not match. Server: ..., Client: ...
という警告が出るので気づきやすいですが、静的サイト生成時だと表示されません。逆に、静的サイト生成の方で試すとエラーは出ませんが違いが大きいため分かりやすいです。
適当にステートを更新して明示的に再描画すると、それ以降ふたつの値はちゃんと一致するようになります。
export default function Home() {
const now = Number(new Date());
const [_, setHoge] = React.useState();
return (
<>
<p><a href={now}>{now}</a></p>
<input type="button" value="refresh" onClick={()=>{setHoge(Math.random())}} />
</>
)
}
また、Productionモードで動作させたときもこのような現象は発生しないようです。
原因
どうやらNext.jsのAutomatic Static Optimizationという機能が原因のようです。サーバサイドとクライアントサイド両方で生成が可能な変数で、かつサーバとクライアントとで値が変わってしまう場合に、このような現象が発生するようです。
この問題は、getStaticPropsを使うと値が一致するようになります。
export default function Home({ now2 }) {
return (
<>
<p><a href={now2}>{now2}</a></p>
</>
)
}
export async function getStaticProps() {
const now2 = Number(new Date());
return { props: { now2 } }
}
またはAutomatic Static OptimizationはgetInitialPropsという機能を使うことで無効になります。
export default function Home({ now2 }) {
return (
<>
<p><a href={now2}>{now2}</a></p>
</>
)
}
Home.getInitialProps = async (_) => {
return { now2: Number(new Date()) };
}
ただしこれらの方法を使うと、静的サイトのビルド時に(サーバサイドで)生成された値が更新される事はありません。静的サイト生成を使う場合は特に困りますね。
もし静的サイト生成を使わない場合(サーバサイドレンダリングする場合)は、getServerSidePropsを使えば良いようです。
export default function Home({ now2 }) {
return (
<>
<p><a href={now2}>{now2}</a></p>
</>
)
}
export async function getServerSideProps() {
const now2 = Number(new Date());
return { props: { now2 } }
}
解決方法
componentDidMount
に引っ掛けることで、サーバサイドで描画されないように(クライアントサイドで一度レンダリングが走った後に、実際のDOMが描画されるように)します。useEffect()
を使う場合は下記のような感じです。
export default function Home() {
const now = Number(new Date());
const [isMounted, setIsMounted] = React.useState(false);
React.useEffect(() => setIsMounted(true));
if (!isMounted) return <>Loading...</>;
return (
<>
<p><a href={now}>{now}</a></p>
</>
)
}