🐱

Next.jsでサーバーサイドでのレンダリングを回避する方法

2022/12/21に公開

方法1

useEffectとフラグで回避する。
useEffectはクライアントサイドでのみ実行されるため、SSR時はmountedがtrueにならず、レンダリングされない。

./components/Foo.tsx
const Foo: React.FC = () => {
  const [mounted, setMounted] = useState(false);

  useEffect(()=> {
    setMounted(true);
  }, [])

  if (!mounted) return null;

  console.log(window)

  return <div>Foo!</div>
};

export default Foo;
./pages/home.tsx
import Foo from '../components/Foo';

const Home = () => {
  return (
    <>
      <Head>
        <title>App</title>
      </Head>
      <main>
        <Foo />
      </main>
    </>
  );
}

export default Home;

方法2

dynamic importを使用し、第二引数のオプションで【ssr: false】を設定する

./components/Bar.tsx
const Bar: React.FC = () => {
  console.log(window);

  return <div>Bar!</div>;
}

export default Bar;
./pages/home.tsx
import dynamic from 'next/dynamic';

const Bar = dynamic(() => import('../components/Bar'), {
  ssr: false,
});

const Home = () => {
  return (
    <>
      <Head>
        <title>App</title>
      </Head>
      <main>
        <Bar />
      </main>
    </>
  );
}

export default Home;

応用:Suspense使用時にSSRでPromiseがthrowされるのを回避する

Promiseのthrowを切り替えるパターン

mountedのフラグがtrueの時のみ、Promiseをthrowする。
useEffectはクライアントサイドでのみ実行されるので、SSR時はmountedがtrueにならず、Promiseがthrowされなくなる。

./components/Todos.tsx
type Todo = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

let data: Todo[] | undefined;

const Todos = () => {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!data && mounted) {
    throw axios
      .get('https://jsonplaceholder.typicode.com/todos')
      .then((res) => {
        setTimeout(() => { // loadingを確認するために処理を遅らせる
          data = res.data;
        }, 5000);
      });
  }

  return (
    <ul>
      {data?.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
};

export default Todos;
./pages/home.tsx
import Todos from '../components/Todos';

const Home = () => {
  return (
    <>
      <Head>
        <title>App</title>
      </Head>
      <main>
        <Suspense fallback={<p>loading...</p>}>
          <Todos />
        </Suspense>
      </main>
    </>
  );
};

export default Home;

SuspenseをWrapしたコンポーネントで、回避するパターン

Suspenseの使用時にクライアントサイドでのみレンダリングされるようにする

./components/SuspenseWrapper.tsx
type Props = {
  children: React.ReactNode;
  fallback: React.ReactNode;
};

const SuspenseWrapper: React.FC<Props> = ({
  children,
  fallback,
}) => {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null;

  return (
    <Suspense fallback={fallback}>{children}</Suspense>
  );
};

export default SuspenseWrapper;
./components/Todo.tsx
type Todo = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

let data: Todo[] | undefined;

const Todos = () => {
  if (!data) {
    throw axios
      .get('https://jsonplaceholder.typicode.com/todos')
      .then((res) => {
        setTimeout(() => {
          // loadingを確認するために処理を遅らせる
          data = res.data;
        }, 5000);
      });
  }

  return (
    <ul>
      {data?.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
};

export default Todo;
./pages/home.tsx
import SuspenseWrapper from '../components/SuspenseWrapper';
import Todos from '../components/Todos';

const Home = () => {
  return (
    <>
      <Head>
        <title>App</title>
      </Head>
      <main>
        <SuspenseWrapper fallback={<p>loading...</p>}>
          <Todos />
        </SuspenseWrapper>
      </main>
    </>
  );
};

export default Home;

以上!

参考サイト

Discussion