🐱
Next.jsでサーバーサイドでのレンダリングを回避する方法
方法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