Prevent falsy request from React Router loader function - javascript

I use React Router 6.4.2 and it's API - createBrowserRouter.
I have a default request that should be done only once when user reach main route ('/').
Request requires auth token (Amplify) so i use protected routes. No token - redirect to './auth/.
Router:
const router = createBrowserRouter([
{
element: (
<RequireAuth>
<AppLayout />
</RequireAuth>
),
children: [
{
path: '/',
loader: getDefaultData,
element: <MainPage />,
},
],
},
{ path: '/auth', element: <Authenticator /> },
{
path: '*',
element: <NotFoundPage />,
},
]);
export const AppRouter = () => {
return <RouterProvider router={router} fallbackElement={<AppLoader centered />} />;
};
RequireAuth:
export const RequireAuth = ({ children }: Props) => {
const location = useLocation();
const { route } = useAuthenticator((context) => [context.route]);
if (route !== 'authenticated') {
return <Navigate to="/auth" state={{ from: location }} replace />;
}
return <>{children}</>;
};
GetDefaultData:
export const getDefaultData = async () => {
store.dispatch(
getData({
someVariables,
})
);
};
What i faced: when not authenticated user try to reach main route ('/'), he reach it for a moment before he will be redirected to './auth/ and React Router run getDefaultData from loader that fails on getting-auth-toker step.
What i expect: React Router will skip getDefaultData for that case. Looking for a way how to tell React Router about that in a beautiful way.
P.S. I know that i can add auth check with return inside getDefaultData function (generally it happens but not from scratch but on getting-auth-token).
I know about shouldRevalidate but not sure that it can help me in that case.
UPD. provided a codesandbox for that
https://codesandbox.io/s/amazing-matsumoto-cdbtow?file=/src/index.tsx
Simply try remove '/auth' from url manually and check console.
UPD. created an issue about that https://github.com/remix-run/react-router/issues/9529

Got an answer from Matt Brophy in github:
Fetching is decoupled from rendering in 6.4, so loaders run before any rendering logic. You should lift your "requires authorization" logic out of the RequireAuth component and into your loaders and redirect from there. Redirecting during render is too late 😄
Also note that all loaders for a matched route route run in parallel, so you should check in each loader for now (just like in Remix). We plan to make this easier to do in one place in the future.
Original answer:
https://github.com/remix-run/react-router/issues/9529

Related

How can I create a subrouter with React Router v6?

Here is my current React Router implementation:
const router = createBrowserRouter([
{
path: "/",
element: (
<Page activeNav="home" >
<Home />
</Page>
)
},
{
path: "/about",
element: (
<Page activeNav="about" >
<About />
</Page>
)
},
{
path: "/blog",
element: (
<Page activeNav="blog">
<Blog />
</Page>
)
},
{
path: "/blog/:postName",
element: (
<Page activeNav="blog" >
<Post />
</Page>
),
loader: ({ params }) => params.postName
},
{
path: "/chess",
element: <ChessRouter />
}
])
The last route, /chess is of importance. I am looking to define routes such as /chess/play, /chess/login, /chess/register, etc. My initial idea was to just put another Router as the element for the /chess path and then all those paths would be routed from there. However, that throws an error saying:
You cannot render a <Router> inside another <Router>. You should never have more than one in your app.
I also tried using the children property on the /chess route but this does not render anything when I go to /chess/play etc.
What is the correct way of implementing subpaths (not sure of the correct word for it)?
It is correct that you cannot render a router component within another router component as this is an invariant violation. You need only one router and routing context per React app.
To render sub-routes on "/chess" then there are two options:
Render nested routes in the routes configuration declaration. This requires the ChessRouter component to render an Outlet component for nested routes to render their element content into. Nested routes will be able to use the new RRDv6.4 Data APIs.
const router = createBrowserRouter([
...,
{
path: "/chess",
element: <ChessRouter />,
children: [
...,
{
path: "play",
element: <ChessPlay />
},
... other chess sub-rotues
],
}
]);
const ChessRouter = () => {
...
return (
...
<Outlet />
...
);
};
Render a root route with trailing wildcard ("*") character that allows descendent routes to also be matched. This allows the ChessRouter component to render descendent routes, i.e. a Routes component with a set of Route components. Descendent routes will not be able to use the RRDv6.4 Data APIs.
const router = createBrowserRouter([
...,
{
path: "/chess/*",
element: <ChessRouter />,
}
]);
const ChessRouter = () => {
...
return (
...
<Routes>
...
<Route path="/play" element={<ChessPlay />} />
... other chess sub-rotues
</Routes>
...
);
};
If your ChessRouter does not contain any additional functionality besides route declarations, you could drop it altogether and use an index route.
Working off of what Drew had:
const router = createBrowserRouter([
...,
{
path: "/chess",
children: [{
index: true,
element: <ChessPlay /> // The default component to load at '/chess'
},
{
path: "play",
element: <ChessPlay />
},
... other chess sub-rotues
],
}
]);
This will let all of your child routes under /chess work as well as prevent an empty page from showing if a user hits the base /chess route.

redirect all `*` to specific path in nextjs

I have a single landing page nextJs application it is possible to redirect all * to specific routes as we do in react-router like this how can I do exactly the same in nextJs
<BrowserRouter>
<Routes>
<Route path={ROUTES.ROOT} element={<Registry />} />
<Route path={ROUTES.ALL} element={<Navigate to={ROUTES.ROOT} />} />
</Routes>
</BrowserRouter>
export const ROUTES = {
ALL: '*',
ROOT: '/registry',
};
what I have done so far is that I'm able to redirect a specific route to specific route but not able to redirect all routes to a specific route
const nextConfig = {
async redirects() {
return [
{
source: '/path', // not able to "*" the route
destination: '/registry', // Matched parameters can be used in the destination
permanent: false,
},
];
},
};
module.exports = nextConfig;
Unfortunately, nextJs doesn't seems to have a proper way to handle this kind of redirection inside the nextConfig, But if you want to redirect any 404 page to home, what you can do is:
Create a custom 404 page inside the pages, note that your page must be named as 404
Add this snippet in the 404 file.
import { useEffect } from 'react'
import { useRouter } from 'next/router'
export default function Custom404() {
const router = useRouter()
useEffect(() => {
router.replace('/')
})
return null
}
With that any not found route should redirect to home page.
See this discussion on github
edit:
One last thing, if you want to handle some kind of logic when a user visit some route and redirect if fail, you can do so with getServerSideProps:
Add the async function getServerSideProps in the page where you want to handle some kind of logic before render the page:
// Page where you want to handle the logic
// data is the props that comes from getServerSideProps
function Page({ data }) {
// Render data...
}
// This gets called on every request
export async function getServerSideProps() {
// fetch some data from external API
const res = await fetch(`https://someapi/fetchData`)
const data = await res.json()
if(!data) {
// Here we redirect the user to home if get some error at the API
return {
redirect: {
destination: '/',
permanent: false
}
}
}
// Otherwise pass the data to Page as props
return { props: { data } }
}
export default Page
It's just an example but you got the idea, if you want to learn more about this, read the docs here

How I could test location with MemoryRouter on React Router DOM v6?

Using React Router DOM (v6) with React Testing Library, I would like to know if there's a way to deal with "mocked" history from createMemoryHistoryor an alternative like it.
Based on the example of Testing Library, I could mock the history on the Router component but in v6 this component doesn't accept the history prop anymore.
So, my doubt is: how can I test history and location with RTL (React Testing Library) with React Router DOM v6? Should I keep using the MemoryRouter?
The recommended testing method using React Router DOM v6.4.0 relies on createMemoryRouter which will return a RemixRouter. Instead of using history through createMemoryHistory the state object of the Router from createMemoryRouter can be used since this Router uses its own history in memory. The state object contains location which we can check for proper navigation.
Using the RouterProvider from React Router DOM we can pass the router made by createMemoryRouter to the provider and along to our tests.
import { render, waitFor } from '#testing-library/react'
import userEvent from '#testing-library/user-event'
import {
createMemoryRouter,
RouterProvider,
useNavigate,
} from 'react-router-dom'
import { screen } from '#testing-library/react'
// The element we want to render. Uses the hook useNavigate to send us somewhere.
const ElementToRender: React.FC = () => {
const navigate = useNavigate()
return <button onClick={() => navigate('/')}>Navigate to Home</button>
}
const setupMyTest = () => {
const router = createMemoryRouter(
[
{
path: '/',
element: <>Navigated from Start</>,
},
{
path: '/starting/path',
// Render the component causing the navigate to '/'
element: <ElementToRender />,
},
],
{
// Set for where you want to start in the routes. Remember, KISS (Keep it simple, stupid) the routes.
initialEntries: ['/starting/path'],
// We don't need to explicitly set this, but it's nice to have.
initialIndex: 0,
}
)
render(<RouterProvider router={router} />)
// Objectify the router so we can explicitly pull when calling setupMyTest
return { router }
}
it('tests react router dom v6.4.0 navigates properly', async () => {
const { router } = setupMyTest()
// Given we do start where we want to start
expect(router.state.location.pathname).toEqual('/starting/path')
// Navigate away from /starting/path
userEvent.click(screen.getByRole('button', { name: 'Navigate to Home' }))
// Don't forget to await the update since not all actions are immediate
await waitFor(() => {
expect(router.state.location.pathname).toEqual('/')
})
})
Using react router dom v6, Router use navigator prop to accept history.
const history = createMemoryHistory();
...
<MockedProvider mocks={[]}>
<Router navigator={history} location={"/"}>
<Home />
</Router>
</MockedProvider>
...
await waitFor(() => {
expect(history.location.pathname).toBe("/");
});

Using client-only routes with page templates coming from Contentful

Goal
I am looking to use client-only routes for content under a certain URL (/dashboard). Some of this content will be coming from Contentful and using a page template. An example of this route would be {MYDOMAIN}/dashboard/{SLUG_FROM_CONTENTFUL}. The purpose of this is to ensure projects I have worked on at an agency are not able to be crawled/accessed and are only visible to 'employers' once logged in.
What I have tried
My pages are generated via gatsby-node.js. The way of adding authentication/client-only routes has been taken from this example. Now the basics of it have been setup and working fine, from what I can tell. But the private routes seem to only work in the following cases:
If I'm logged in and navigate to /dashboard
I'm shown Profile.js
If I an not logged in and go to /dashboard
I'm shown Login.js
So that all seems to be fine. The issue comes about when I go to /dashboard/url-from-contentful and I am not logged in. I am served the page instead of being sent to /dashboard/login.
exports.createPages = async ({graphql, actions}) => {
const { createPage } = actions;
const { data } = await graphql(`
query {
agency: allContentfulAgency {
edges {
node {
slug
}
}
}
}
`);
data.agency.edges.forEach(({ node }) => {
createPage({
path: `dashboard/${node.slug}`,
component: path.resolve("src/templates/agency-template.js"),
context: {
slug: node.slug,
},
});
});
}
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions;
if(page.path.match(/^\/dashboard/)) {
page.matchPath = "/dashboard/*";
createPage(page);
}
};
My auth.js is setup (the username and password are basic as I am still only developing this locally):
export const isBrowser = () => typeof window !== "undefined";
export const getUser = () =>
isBrowser() && window.localStorage.getItem("gatsbyUser")
? JSON.parse(window.localStorage.getItem("gatsbyUser"))
: {};
const setUser = (user) =>
window.localStorage.setItem("gatsbyUser", JSON.stringify(user));
export const handleLogin = ({ username, password }) => {
if (username === `john` && password === `pass`) {
return setUser({
username: `john`,
name: `Johnny`,
email: `johnny#example.org`,
});
}
return false;
};
export const isLoggedIn = () => {
const user = getUser();
return !!user.username;
};
export const logout = (callback) => {
setUser({});
call
};
PrivateRoute.js is setup the following way:
import React from "react";
import { navigate } from "gatsby";
import { isLoggedIn } from "../services/auth";
const PrivateRoute = ({ component: Component, location, ...rest }) => {
if (!isLoggedIn() && location.pathname !== `/dashboard/login`) {
navigate("/dashboard/login");
return null;
}
return <Component {...rest} />;
};
export default PrivateRoute;
dashboard.js has the following. The line <PrivateRoute path="/dashboard/url-from-contentful" component={Agency} />, I have tried a couple of things here - Statically typing the route and using the exact prop, using route parameters such as /:id, /:path, /:slug :
import React from "react";
import { Router } from "#reach/router";
import Layout from "../components/Layout";
import Profile from "../components/Profile";
import Login from "../components/Login";
import PrivateRoute from "../components/PrivateRoute";
import Agency from "../templates/agency-template";
const App = () => (
<Layout>
<Router>
<PrivateRoute path="/dashboard/url-from-contentful" component={Agency} />
<PrivateRoute path="/dashboard/profile" component={Profile} />
<PrivateRoute path="/dashboard" />
<Login path="/dashboard/login" />
</Router>
</Layout>
);
export default App;
And finally agency-template.js
import React from "react";
import { graphql, Link } from "gatsby";
import styled from "styled-components";
import SEO from "../components/SEO";
import Layout from "../components/Layout";
import Gallery from "../components/Gallery";
import GeneralContent from "../components/GeneralContent/GeneralContent";
const agencyTemplate = ({ data }) => {
const {
name,
excerpt,
richDescription,
richDescription: { raw },
images,
technology,
website,
} = data.agency;
const [mainImage, ...projectImages] = images;
return (
<>
<SEO title={name} description={excerpt} />
<Layout>
<div className="container__body">
<GeneralContent title={name} />
<Gallery mainImage={mainImage} />
<GeneralContent title="Project Details" content={richDescription} />
<div className="standard__images">
<Gallery projectImages={projectImages} />
</div>
<ViewWebsite>
<Link className="btn" to={website}>
View the website
</Link>
</ViewWebsite>
</div>
</Layout>
</>
);
};
export const query = graphql`
query ($slug: String!) {
agency: contentfulAgency(slug: { eq: $slug }) {
name
excerpt
technology
website
images {
description
gatsbyImageData(
layout: FULL_WIDTH
placeholder: TRACED_SVG
formats: [AUTO, WEBP]
quality: 90
)
}
richDescription {
raw
}
}
}
`;
export default agencyTemplate;
I assume that gating content from a CMS is possible with Gatsby but I might be wrong given it is an SSG. I may be misunderstanding the fundamentals of client-only. The concepts in React and using Gatsby are still very new to me so any help or guidance in achieving the goal would be appreciated.
What I ended up doing
So the answer I marked was the one that 'got the ball rolling'. The explanation of what was happening with state and requiring either useContext or redux helped me understand where I was going wrong.
Also, the suggestion to use web tokens prompted me to find more information on using Auth0 with the application.
Once I had got out of the mindset of creating pages using Gatsby (Through a template, via gatsby-node.s), and instead doing it in a 'React way' (I know Gatsby is built with React) by handling the routing and GraphQL it became clearer. Along with the authentication, all I ended up doing was creating a new <Agency /> component and feeding the data from GraphQL into it and updating the path with my map().
return (
<>
<Router>
<DashboardArea path="/dashboard/" user={user} />
{agencyData.map(({ node }, index) =>
node.slug ? (
<Agency key={index} data={node} path={`/dashboard/${node.slug}`} />
) : null
)}
</Router>
</>
);
I assume that in your PrivateRoute component, you're using the isLoggedIn check incorrectly. importing and using isLoggedIn from auth.js will run only initially and will not act as a listner. What you can do is that store the value of isLoggedin in global state variable like(useContext or redux) and make a custom hook to check for the login state. Secondly avoid accessing localStorage directly, instead use the global state managment (useContext, redux) or local state managment (useState, this.state).
Note: that when ever you go to a route by directly pasting url in browser, it always refreshes the page and all your stored state is reinitialized. This may be the reason why you may be experiencing this issue. The browser does not know that you had been previously logged in and therefore it always validates once your application is mounted. What you can do is that you can store isLoggedIn state in browser's localstore. Personally I like to use redux-persist for that.
export const useGetUser = () => { //add use infront to make a custom hook
return useSelector(state => state.gatsByUser) // access user info from redux store
};
export const handleLogin = ({ username, password }) => {
//suggestion: don't validate password on client side or simply don't use password,
//instead use tokens for validation on client side
if (username === `john` && password === `pass`) {
dispatch(setUserInfo({
username: `john`,
name: `Johnny`,
email: `johnny#example.org`,
isLoggedIn: true,
}));
return true;
}
return false;
};
// adding 'use' infront to make it a custom hook
export const useIsLoggedIn = () => {
//this will act as a listner when ever the state changes
return useSelector(state => state.gatsByUser?.isLoggedIn ?? false);
};
export const logout = (callback) => {
const dispatch = useDispatch(); // redux
dispatch(clearUserInfo());
};
Now in private route do
import React from "react";
import { navigate } from "gatsby";
import { useIsLoggedIn } from "../services/auth";
const PrivateRoute = ({ component: Component, location, ...rest }) => {
const isLoggedIn = useIsLoggedIn();
if (!isLoggedIn) {
return navigate("/dashboard/login");
}
return <Component {...rest} />;
};
export default PrivateRoute;
It looks like you're server-side rendering dashboard/[url] in gatsby-node.js/createPages()? IIRC those routes will have higher precedence than dynamic routes (which you specify with #reach/router in dashboard.js).
Plus, the content of those routes are currently publicly available. If you want to keep them truly private, you should query Contentful graphql API directly on the client side (via fetch() or use apollo client, urql, etc.), instead of relying on Gatsby's graphql server.
I would do the follows:
Removing the dashboard/[url] portion in your gatsby-node.js
Configure your web host so that all routes matches '/dashboard/*' will redirect to '/dashboard'
If you happen to host your static site on Netlify, you'd create a _redirects with this, assuming you configure Gatsby to create nice url:
# /static/_redirect
/dashboard/* /dashboard 200
A possible simpler way that match your current setup is gating content at web host level. You can configure nginx to protect /dasboard/* with basic auth. However maintaining/updating password is a pain & modern hosting solution don't really allow user to configure that.
Netlify offers its own authentication solution that you could look into.
I've had the same issue earlier and I couldn't get exact functionality with Private Routes.
In my case, I created two separate Layouts for Public and Private Routes and built the authentication to Private Layout. Logged-in user data were linked to a redux store (First I used Context, then moved to Redux). In Private routes with the Private Layout, it redirected the guest users to the Login page and redirected them to the same page after login.
Private layout is something like this:
import React from "react";
import { navigate } from "gatsby";
import { useSelector } from "react-redux";
const PrivateLayout = ({children}) => {
const isLoggedIn = useSelector(state => state.user.isLoggedIn);
useEffect(() => {
if (!isLoggedIn) {
// redirect the user to login page.
// I'm sending the current page's URL as the redirect URL
// so that I can take the user back to this page after logging in.
}
}, [isLoggedIn])
if (!isLoggedIn) return null;
return <>
{...header}
{children}
{...footer}
</>
}
export default PrivateLayout;
Not sure if this workaround suits you. If it does, I can give you more info.

Next.js: Router.push with state

I'm using next.js for rebuilding an app for server side rendering.
I have a button that handles a search request.
In the old app, the handler was this one:
search = (event) => {
event.preventDefault();
history.push({
pathname: '/results',
state: {
pattern: this.state.searchText,
}
});
}
In the results class, I could get the state date with this.props.location.state.pattern.
So now I'm using next.js:
import Router, { withRouter } from 'next/router'
performSearch = (event) => {
event.preventDefault();
Router.push({ pathname: '/results', state: { pattern: this.state.searchText } });
};
In the results class, I use
static async getInitialProps({req}) {
return req.params;
}
I'm not sure if I have to add this to my server.js:
server.get('/results', (req, res) => {
return app.render(req, res, '/results', req.params)
})
However, the function getInitialProps throws an error because req is undefined. Long text, short question: how to pass state or params to another page without using GET parameters?
In next.js you can pass query parameters like this
Router.push({
pathname: '/about',
query: { name: 'Someone' }
})
and then in your next page (here in /about page), retrieve the query via the router props, which needs to be injected to Component by using withRouter.
import { withRouter } from 'next/router'
class About extends React.Component {
// your Component implementation
// retrieve them like this
// this.props.router.query.name
}
export default withRouter(About)
If you want your url remain clean, make a small addition to Prithwee Das's answer like below.
Router.push({
pathname: '/about',
query: { name: 'Someone' }
}, '/about');
Now you can access props in your component using props
...
const YourComponent = (props) => {
useEffect(() => {
console.log(props.router.query.name);
}, [props.router.query]);
return (
<React.Fragment>
...
</React.Fragment>
);
};
...
I don't know whether this supports SSR, but I had to do it as follows to avoid the error cannot read property 'query' of undefined.
This uses useRouter hook to get access to the url, imported as below.
import { useRouter } from 'next/router'
Assume you want to pass data {name:'Someone'} from Component A to Component B.
In Component A,
const router = useRouter();
router.push(
{ pathname: "/path_of_component_b", query: { name: "Someone" } },
"path_of_component_b"
);
In Component B,
const router = useRouter();
useEffect(() => {
alert(router.query.name); // Alerts 'Someone'
}, [router.query]);
If you want 'clean' urls, one way to go about it is to add onClick handler to your link and store required information in context/redux store. It easy to implement if you already have one.
<Link href='...'>
<a onClick={()=>{dispatch(....)}}>Link<a/>
<Link>

Categories

Resources