Authenticated loaders #9327
Replies: 9 comments 37 replies
-
would really like to kknow the answer to this as well., Looked over the documents and see no mention of this. Basically need to make sure config and auth is ran BEFORE any sub routes. right now they are run in parallel and its making fetch calls with-out having the bearer tokens |
Beta Was this translation helpful? Give feedback.
-
Adding this here for those stumbling onto this conversation Might be worth nothing how other frameworks do it: |
Beta Was this translation helpful? Give feedback.
-
Hey folks! At the moment, just like Remix, all loaders are called in parallel and you should be checking stuff like auth state in all of your loaders. This is also important because with nested routing, re-used routes will not have their loaders re-executed. So if you only check in the We have some ideas floating around internally on ways to solve this, so we'll update this discussion as soon as we start to solidify our ideas there. |
Beta Was this translation helpful? Give feedback.
-
We're designing the middleware API right now that will meet this use case and will be publishing an RFC soon. You can see its progress on the roadmap here: Roadmap (view) |
Beta Was this translation helpful? Give feedback.
-
Something like this can work: let resolveAuthenticatedPromise: (result: unknown) => void;
let authenticatedPromise = new Promise((resolve) => {
resolveAuthenticatedPromise = resolve;
});
/** Call this function when the user is authenticated */
export function authenticateUser() {
resolveAuthenticatedPromise(undefined);
}
/**
* Makes sure the user is authenticated, useful to avoid 401 calls to the api
*
* @example
* const loader = async () => {
* return defer({ data: ensureAuthenticated().then(() => fetchData()) });
* };
*/
export async function ensureAuthenticated() {
return authenticatedPromise;
} |
Beta Was this translation helpful? Give feedback.
-
How does everyone do the permissions module? export const routers= [
{ path: '/', element: <Navigate to="/realtime" replace /> },
{ path: '/login', element: <Login /> },
{
element: <BasicLayout />,
children:[]
},
{ path: '*', element: <NotFound /> },
]
const BasicLayout =()=> {
const {userInfo, isLoading,fetchUserInfo} = useStore() // zustand
let location = useLocation();
useEffect(() => {
if (!localStorage.getItem('token')) {
navigate('/login')
}
if(!userInfo){
fetchUserInfo()
}
//... also fetch other global data
}, [location.key])
if (isLoading || otherGlobalDataLoading) {
return <LoadingOverlay />
}
return <MyErrorBoundary>
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
</MyErrorBoundary>
}
const App = ()=> {
return <Suspense>
<RouterProvider router={_router} />
</Suspense>
}
// axios.ts
import { router } from './App'
router.navigate('/login') // token 401
localstorage.delete('token') // option
// Login.tsx
// Refresh the page directly, this is the easiest way, otherwise you have to reset all the state, e.g. userInfo globalData
const doLogin => window.location.replace('/') |
Beta Was this translation helpful? Give feedback.
-
I've found a good approach I think. Don't import your routes until your App is ready. Import your AppRoutes component lazy/Suspense Edited 10/24/2024: Check the version 2 without lazy/Suspense #version2. import { lazy, Suspense } from 'react';
import AppTheme from '@components/layout/theme/AppTheme';
import AppSnackbar from './components/ui/AppSnackbar';
import './styles/App.css';
import LoadingApp from '@components/ui/LoadingApp';
import { useInitApp } from '@hooks/useInitApp';
// Don't load AppRoutes until Auth/App is loaded
const AppRoutes = lazy(() => import('./routes/AppRoutes'));
const App = () => {
const { isLoaded } = useInitApp();
if (!isLoaded) {
return <LoadingApp />;
}
return (
<AppTheme>
{/* Use Suspense for lazy AppRoutes - So loaders are not going to be called */}
{/* const router = createBrowserRouter([...]) inside AppRoutes will be defined when the app is ready. */}
<Suspense>
<AppRoutes />
</Suspense>
<AppSnackbar />
</AppTheme>
);
};
export default App; Then your AppRoutes component should be like this. I'm using lazy Component by react router as well. You can use import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import AppLayout from '@components/layout/AppLayout/AppLayout';
import { isLoggedGuard, loaderAuthGuard } from './guards';
import { tcLoader } from '@loaders/tcLoader';
import { loadDashboard } from '@loaders/dashboardLoader';
const router = createBrowserRouter([
// PRIVATE ROUTES
{
element: <AppLayout />,
loader: loaderAuthGuard(),
children: [
{
path: '/dashboard',
lazy: () =>
import('@pages/dashboard/DashboardPage').then((module) => ({
Component: module.default,
})),
loader: loaderAuthGuard(loadDashboard),
},
{
path: '/user-profile',
lazy: () =>
import('@pages/userprofile/UserProfilePage').then((module) => ({
Component: module.default,
})),
loader: loaderAuthGuard(),
},
],
},
{
path: '/tc',
lazy: () =>
import('@pages/tc/TermsConditionsPage').then((module) => ({
Component: module.default,
})),
loader: loaderAuthGuard(tcLoader),
},
// PUBLIC ROUTES
{
path: '/',
lazy: () =>
import('@pages/welcome/WelcomePage').then((module) => ({
Component: module.default,
})),
},
{
path: '/login',
lazy: () =>
import('@pages/login/LoginPage').then((module) => ({
Component: module.default,
})),
loader: isLoggedGuard,
},
]);
const AppRoutes = () => <RouterProvider router={router} />;
export default AppRoutes; Then the guard.ts file import { LoaderFunction, LoaderFunctionArgs, redirect } from 'react-router-dom';
import { WindowWithClerk } from '@models/app';
export const loaderAuthGuard =
<T extends LoaderFunction>(load?: T): LoaderFunction<T> =>
async (args: LoaderFunctionArgs) => {
const isLogged = ...;
if (isLogged) {
return load ? await load(args) : null;
} else {
throw redirect('/login');
}
};
export function isLoggedGuard() {
const isLogged = ...;
if (isLogged) {
throw redirect('/dashboard');
}
return null;
} |
Beta Was this translation helpful? Give feedback.
-
Just regarding the original question, it seems like middleware, which is still unreleased (but supported in express), is the right approach. |
Beta Was this translation helpful? Give feedback.
-
I think it could be like this: export function protectedLoader<T extends LoaderFunction<any>>(load: T): Awaited<T> {
const loader = async(args: LoaderFunctionArgs) => {
const authenticated = authStore.authenticated
if (authenticated) {
return await load(args)
}
else {
throw redirect(config.REDIRECT.LOGIN)
}
}
return loader as unknown as Awaited<T>
} You still get the "type", examples of usage: |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
It's my understanding that all matched loaders are fetched in parallel. Is there a best practice for solving the problem that we only want to execute certain loaders if a user is authenticated?
Beta Was this translation helpful? Give feedback.
All reactions