Help us understand the problem. What is going on with this article?

Next.jsで、同じ変数なのに属性内とテキスト内とで違う値になる

環境

  • Next.js 9.4.4
  • React 16.13.1

再現手順

Next.jsで、下記のようなコンポーネントを定義します。そしてnext devで開発サーバを立ち上げるか、next build && next exportで静的サイト生成して、サイトを表示します。

pages/index.js
export default function Home() {
  const now = Number(new Date());
  return (
    <>
      <p><a href={now}>{now}</a></p>
    </>
  )
}

このときのnow変数で表示される値が、href属性とaタグ内部テキストとで同じ変数なのに異なる値を取ってしまいます

image.png

ふたつの値は、それぞれ下記のものになっています。

  • href属性内の値は、サーバサイドでのページ生成時の値(静的サイトのビルド時の値)
  • テキストは、クライアントサイドでのページ表示時の値

開発サーバ立ち上げ時はブラウザのコンソールにprop did not match. Server: ..., Client: ...という警告が出るので気づきやすいですが、静的サイト生成時だと表示されません。逆に、静的サイト生成の方で試すとエラーは出ませんが違いが大きいため分かりやすいです。

適当にステートを更新して明示的に再描画すると、それ以降ふたつの値はちゃんと一致するようになります。

pages/index.js
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を使うと値が一致するようになります。

pages/index.js
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という機能を使うことで無効になります。

pages/index.js
export default function Home({ now2 }) {
  return (
    <>
      <p><a href={now2}>{now2}</a></p>
    </>
  )
}

Home.getInitialProps = async (_) => {
  return { now2: Number(new Date()) };
}

ただしこれらの方法を使うと、静的サイトのビルド時に(サーバサイドで)生成された値が更新される事はありません。静的サイト生成を使う場合は特に困りますね。

もし静的サイト生成を使わない場合(サーバサイドレンダリングする場合)は、getServerSidePropsを使えば良いようです。

pages/index.js
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()を使う場合は下記のような感じです。

pages/index.js
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>
    </>
  )
}

参考

suzuki_sh
Windowsでコンピュータの世界が広がります
https://www.s2terminal.com
finergy-a-tm
大阪府大阪市北区角田町8番1号 梅田阪急ビル オフィスタワー35F
https://finergy.a-tm.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした