Introduction
Next.js melakukan prerender untuk static page, kemudian baru melakukan hydrate ke interaktivitas yang full secara client side.
Dengan konsep tersebut, maka kita akan melihat halaman HTML dan CSS terlebih dahulu, kemudian beberapa detik kemudian, kita mendapatkan JavaScript, dan semua interaktivitas seperti click button.
The Problem
Pada Create React App, melakukan redirect atau history.push
bukan sebuah masalah, karena semua data difetch pada client-side, sehingga kita akan mendapatkan file lengkap dengan Javascriptnya. Flashing content otomatis tidak menjadi masalah.
Tetapi, di Next.js kita mendapatkan static page, kemudian setelah hydration, code JavaScript untuk redirect baru akan berjalan. Ini menjadi masalah karena akan ada content yang muncul untuk beberapa detik sebelum aplikasi di redirect. Hal ini tentunya tidak diinginkan terutama untuk konten yang tidak boleh dilihat oleh unauthorized user.
Saya melihat problem ini masih ada beberapa di production app, beberapa memang menutupi data" penting karena biasanya di fetch secara client-side, tetapi kadang shell dari kontennya masih muncul untuk waktu sebentar. Coba buka website ini app.splitbee.io/projects. Kamu seharusnya tidak memiliki akses ke website ini, tetapi karena problem tersebut, kita bisa melihat dashboard shell muncul sebentar, baru diarahkan ke /login page
Sebenarnya ada cukup banyak jawaban di internet untuk menggunakan method seperti server-side rendering, dan memanfaatkan cookies dengan menggunakan dangerouslySetInnerHTML
di Head tag.
Method yang saya gunakan ini tidak perlu hal" tersebut, tetapi kita wajib menggunakan full-page loader untuk menutupi kontennya.
Solution
Saya membuat demo di https://learn-auth-redirect-nextjs.vercel.app/
Anda bisa mencoba membuka page tersebut, dan langsung mengunjungi learn-auth-redirect-nextjs.vercel.app/blocked di new tab. Kamu akan secara singkat melihat loader, kemudian diredirect ke '/' tanpa melihat konten di dalamnya sama sekali.
Ada 2 approach yang saya temukan.
1. Melakukan checking di setiap page
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '@/contexts/auth';
import FullPageLoader from '@/components/FullPageLoader';
export default function blocked() {
const router = useRouter();
const { isAuthenticated, user, logout, isLoading } = useAuth();
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push('/');
}
}, [isAuthenticated, isLoading]);
if (isLoading || !isAuthenticated) {
return <FullPageLoader />;
}
return (
<div className='layout space-y-4 py-12'>
<h1>YOUR CONTENT THAT SHOULD NOT BE SEEN UNLESS AUTHENTICATED</h1>
</div>
);
}
Disini, kita mendapatkan nilai isAuthenticated
dari auth context, kamu bisa melihat repositori untuk melihat code lebih lengkap.
Dari code diatas, akan mereturn FullPageLoader componet terlebih dahulu sambil menunggu page render, dan menunggu auth context selesai mengecek token ke API menggunakan useEffect.
Potongan code ini menggunakan useEffect di authentication context, untuk memverifikasi token yang biasanya disimpan di localStorage. Jika kamu ingin mengetahu Authentication Context pattern lebih lanjut, saya memiliki code snippet tentang itu.
Context juga mengembalikan isLoading value, dan kita menampilkan loader ketika sedang loading, sampai kita mendapat value isAuthenticatednya
Pattern ini akan secara efektif memblock content yang tidak ingin kita tampilkan ke unauthorized users. Tetapi, dengan menggunakan first approach ini, akan menyulitkan jika kita harus menambahkan pattern useEffect pada setiap pagenya. Maka saya membuat component PrivateRoute, mirip seperti React Router patternnya CRA.
2. Menggunakan PrivateRoute Component
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '@/contexts/auth';
import FullPageLoader from './FullPageLoader';
export default function PrivateRoute({ protectedRoutes, children }) {
const router = useRouter();
const { isAuthenticated, isLoading } = useAuth();
const pathIsProtected = protectedRoutes.indexOf(router.pathname) !== -1;
useEffect(() => {
if (!isLoading && !isAuthenticated && pathIsProtected) {
// Redirect route, you can point this to /login
router.push('/');
}
}, [isLoading, isAuthenticated, pathIsProtected]);
if ((isLoading || !isAuthenticated) && pathIsProtected) {
return <FullPageLoader />;
}
return children;
}
Dengan menggunakan komponen ini, kita bisa menyantumkan route yang mau kita protect di _app.js
//_app.js
import SEO from '@/next-seo.config';
import '@/styles/globals.css';
import { AuthProvider } from '@/contexts/auth';
import PrivateRoute from '@/components/PrivateRoute';
function MyApp({ Component, pageProps }) {
// Add your protected routes here
const protectedRoutes = ['/blocked-component'];
return (
<>
<AuthProvider>
<PrivateRoute protectedRoutes={protectedRoutes}>
<Component {...pageProps} />
</PrivateRoute>
</AuthProvider>
</>
);
}
export default MyApp;
I'm open for all suggestions and contributions to improve 🚀. Open a PR on the repository or email me at [email protected]
Demo
1. Tanpa menggunakan full page loader & not authenticated /blocked-unhandled
Dapat kita lihat, tulisan text merah masih bisa kita lihat.
2. Menggunakan full page loader & not authenticated /blocked-component
Dengan menggunakan full page loader, maka tidak ada konten yang akan ke flash
3. Menggunakan full page loader & authenticated dengan cara mengecek token localStorage
Menggunakan full page loader akan tetap berjalan ketika user sudah memiliki token di localStorage