I will be listing the issues and its solutions which I faced while making this app. Its a minimal app with features like authentication, CRUD for few initial features:
- Brands
- Categories and Subcategories
- HSN Codes
- Products
This is an app which I made for the initial stage of a ecommerce app’s admin side. So, nothing much of a features but the learnings are more of a setup and start the app from scratch with new and updated libraries as of today.
Libraries used:
- Zod: For form schema validation
- Zustand: For state management
- ky: For api layer
- date-fns: For handling dates
- React Router v7: For handling routes
- Tanstack Form: For Big form handling
- Tanstack Query: For api data layer
- Tanstack Table: For rendering data tables
- Shadcn: For UI design system and components
prerequisites/development needs
- Typescript v6
- Node v22
- pnpm
- vite
API Layer
I needed some basic setup like:
- An api client instance
- So, the reason is I wanted it to be flexible, as I might connect to different backends, may be one for basic listing and mutations(Node) and other for analytics(Python)
- It should have baseUrl where I can configure the backend origin
- Retry mechanism
- If some api fails, it should retry for certain times(like 3 times) before it stops trying
- Intercepting:
- Adding auth token in headers before sending api request
- Also, after response, we should be able to check the response and retry if we want
- if token gets expired, while fetching/mutating, then we can get a new token and then retry the failed request using the new access token
A good issue I got while building this layer was the retry mechanism. So, when a api fails due to unauthorization, ky gets the new token using the refresh token and then retries the failed request again. But while creating product I had to fetch the list of existing brands, hsn codes and subcategories which I was fetching using Promise.all() simultaneously. When all the 3 requests failed at once, the new access tokens also fetched thrice which is an issue. So I had to separate the fetching of new access token using a separate ky instance. This fetching returns a Promise which acts as an active Promise if its going on, then subsequent retries checks if there is any active request, if yes it references the same active promise. This is something new which I experienced. Below is the code:
const handleNewTokensFetch = () => { let activePromise: Promise<ApiResponse<AuthTokens>>|null = null const fetchNewTokens = async (oldRefreshToken: string): Promise<ApiResponse<AuthTokens>> => { if (activePromise) return activePromise const url = `${APP_ENV_CONFIG.API_BASE_URL}auth/get-tokens` activePromise = ky.post(url, {json: {refreshToken: oldRefreshToken}}).json<ApiResponse<AuthTokens>>() const tokens = await activePromise return tokens } const resetActiveFetchings = () => { activePromise = null } return {fetchNewTokens, resetActiveFetchings}}const {fetchNewTokens, resetActiveFetchings} = handleNewTokensFetch()
So, as you can see, handleNewTokensFetch() returns 2 functions, fetchNewTokens() and resetActiveFetchings(). I have to call only the former one and everything is taken care of. Internally, it checks for activePromise, and if it is present, it returns that else calls the backend for new tokens. This function will be called when we get 401 status for unauthorized access as a response. Following is its usage:
try { // we do not need to try/catch as if error happens it will throw to the old request which returned 401 const tokenData = await fetchNewTokens(oldRefreshToken)} catch (error) { window.location.replace(window.location.origin); throw error} finally { resetActiveFetchings()}
Route Layer
One challenge which I got in this layer was guarding routes. As, I am using React Router v7, I had to guard the routes so that unuathorized access cannot happen. This seems really easy, but there was a catch. And I was using the createBrowserRouter([]) rather than the old BrowserRouter for this app.
const router = createBrowserRouter([ { path: APP_ROUTES.HOME, element: <Navigate to={APP_ROUTES.LOGIN} replace />, }, { path: "auth", children: [ { path: 'login', element: <LoginPage /> }, { path: 'register', element: <RegisterPage /> } ], }, { element: <SidebarLayout />, children: [ { path: 'dashboard', hydrateFallbackElement: <div>Loading Dashboard...</div>, element: <DashboardPage /> }, { path: 'brands', hydrateFallbackElement: <div>Loading Brands...</div>, async lazy() { const module = await import('./pages/brands/BrandsPage.page') return { Component: module.default }; } }, ] }, { // 3. Catch-all: Sends any dead/unmatched links to not found page path: "*", element: <NotFoundPage />, },])
As you can see I have auth routes and the dashboard and other module routes. Its pretty common setup. But, I dont have any guards which mean, even if user is not authenticated, can access the dashboard.
I got to know there is something called middleware in react router which gets executed before we reach to the element. Basic concept. And, I implemented it.
element: <SidebarLayout />,
middleware: [authMiddleware],
export async function authMiddleware() { const accessToken = useTokensStore.getState().accessToken if (!accessToken) { return redirect(APP_ROUTES.LOGIN); } const userId = useAuthUserStore.getState().id if (userId) { return } const err = await useAuthUserStore.getState().fetchUserProfile() if (err) { console.log('Error while authenticating user from backend') console.log({err}) toast.error('Session expired. Please, login again') useTokensStore.getState().resetStore() return redirect(APP_ROUTES.LOGIN); } return}
This is the basic setup. I should be using cookies though, but this is the first stage and I will be improving the code later. But, the issue I faced here is a different one. The question here is what will the user be seeing when I am fetching the userprofile? But, what actually was happening is when user directly goes to the dashboard page using address bar, it actually navigates to the dashboard for certain time and then gets redirected to login page.
This is the scenario, when access token has expired but frontend dont know it. When exactly this situation happens? When user logs in and navigates to the dashboard and moves out for some reason and comes back after few hours and then navigates to the brands page. In this scenario, even if the token gets expired, our app does not know, it has access token, it has user profile data all in the memory but while its being checked, user has already moved to brands page and after few seconds when backend returns 401, it actually moves to login page. This is a bad user experience. Thats how middleare works in react router. It should not actually show the brands page, but as it has already moved to the page while the backend call is fetching.
I need some loader for the user to see while the backend call is happening. This loader will also be seen when user refresh the restricted page, coz the same middleware will be called. So, I added a loader component in the App component as a global one which gets activated when this exact backend call happens.
/* NOTE: 1. When we do a page refresh or a hard reload, we check for token validation from a backend call via auth middleware, this loader will be shown while we call the backend api 2. This is when user has not logged out but token has expired and backend is called to verify*/const AuthHydrator = () => { const authenticating = useAuthUserStore(state => state.authenticating) if (authenticating) { return ( <div className="fixed inset-0 z-50 flex min-h-svh w-full items-center justify-center bg-muted p-6 md:p-10"> <div className="w-full max-w-sm text-center"> <Badge variant="outline"> <Spinner data-icon="inline-start" /> Authenticating... </Badge> </div> </div> ) } return null}export default AuthHydrator
I added a small loader component which just shows the user a spinner with a text called Authenticating…
function App() { return ( <> <AuthHydrator /> <Toaster position="top-right" /> <ApiQueryProvider> <TooltipProvider> <AppRoutes /> </TooltipProvider> </ApiQueryProvider> </> )}export default App
This I used in the App component. But, the issue is still not solved. I am showing a loader only for the backend call, but the dashboard still accessible for the small amount of time before the backend returns 401. I thought new react router has something for it and I checked all of them but does not suit me. So, I did implement the declarative one which I was doing earlier with BrowserRouter. The ProtectedLayout.
/* NOTE: This is for client side restriction on page access without involving backend. Generally a loader when someone tries to navigate to protected page when logged out*/const ProtectedLayout = ({element}: {element: ReactNode}) => { const accessToken = useTokensStore(state => state.accessToken) const userId = useAuthUserStore(state => state.id) if (!accessToken && !userId) { return ( <div className="fixed inset-0 z-50 flex min-h-svh w-full items-center justify-center bg-muted p-6 md:p-10"> <div className="w-full max-w-sm text-center"> <Badge variant="outline"> <Spinner data-icon="inline-start" /> Verifying authorization... </Badge> </div> </div> ) } return element}export default ProtectedLayout
This I had to wrap on every the SidebarLayout so that no one can access its children route.
element: <ProtectedLayout element={<SidebarLayout />} />,
middleware: [authMiddleware],
children: [
{
path: 'dashboard',
hydrateFallbackElement: <div>Loading Dashboard...</div>,
element: <DashboardPage />
},
interestting in react router
Something little beautiful about React Router is View Transition
So, I was looking for something little animation so that dashboard does not feel lifeless and as we know we have view transition which helps smooth transition between 2 pages. I wanted that as well. But, as I tried to implement it seems that it needs a separate javascript as we have client side routing and view transition works on server side multiple pages application. But, react router provides it out of the box through its navigate() and <Link />. The viewTransition is the trick
<Link to={url} viewTransition><span>{title}</span></Link>navigate(APP_ROUTES.DASHBOARD, { viewTransition: true })

Just a small change and the app feels good 😁
Leave a comment