LoginSignup
2
1

More than 3 years have passed since last update.

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

Posted at

環境

  • 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>
    </>
  )
}

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1