Programmatically create PrivateRoutes on Gatsby - javascript

I need to create a client's only Blog. I followed gatsby docs to create a site with user authentication, but i need to programmatically create PrivateRoutes based on markdown files.
By now i'm using this component to render PrivateRoutes and they work, but how do I create a PrivateRoute for a dynamically created page if a component doesn't exist. Pages are created at build time with gatsby onCreatePage API but it only uses a template component.
<Layout>
<Router>
<PrivateRoute path="/app/profile" component={Profile} />
<PrivateRoute path="/app/contact" component={ContactPage} />
<Login path="/app/login" />
</Router>
</Layout>
)
Static Pages Routes are created like this:
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
// page.matchPath is a special key that's used for matching pages
// only on the client.
if (page.path.match(/^\/app/)) {
page.matchPath = "/app/*"
// Update the page.
createPage(page)
}
}
And this is what I'm trying to do for blog posts:
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
// page.matchPath is a special key that's used for matching pages
// only on the client.
if (page.path.match(/^\/blog/)) {
page.matchPath = "/blog/*"
// Update the page.
createPage(page)
}
}

Related

react.js & django, useParams unable to navigate to the page

I am current building a react app with django, I am trying to navigate from the HomePage to the DataPage with corrsponding id. However, it return Page not found error. I am using react-router-dom v6.
Using the URLconf defined in robot.urls, Django tried these URL patterns, in this order:
admin/
api/
api-auth/
homepage
homepage/data
The current path, homepage/data/54, didn’t match any of these.
Here is my App.js
export default class App extends Component {
constructor(props) {
super(props);
}
renderHomePage() {
return (
<HomePage />
);
}
render() {
return (
<BrowserRouter>
<Routes>
<Route exact path='homepage/' element={this.renderHomePage()} />
<Route path='homepage/data/:id' element={<DataPage />} />
</Routes>
</BrowserRouter>
)
}
}
const appDiv = document.getElementById("app");
render(<App />, appDiv);
And I want to navigate to the DataPage below:
const EmtpyGrid = theme => ({
Grid: { ... }
});
function DataPage(props) {
const { classes } = props;
const { id } = useParams();
return (
<div>
... some material ui components ...
<div/>
)
};
DataPage.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(EmtpyGrid)(DataPage);
I was thinking whether I need configure my url.py in frontend as well, and I need to define a designated value for {id} returned from the materialui component first. Perhaps I need a button or <Link><Link/> for the navigation instead of just simply typing in the url? Still, no luck after many attempts. I am so confused right now.
If you know what is wrong with my code, please feel free to leave a comment. Thanks
After many tries and checking documents, I don't really need to configure my urls.py. I only things that I am missing is to put a parameter in my naviagate() from onRowClick={((rowData, event) => {navigate('data/');})} to onRowClick={((rowData, event) => {let id = event.sample_ID; navigate('data/' + id)})}; I was thinking the problem too complicated.
Thanks you guys for sharing!

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.

Cannot code nested routes with react-router-dom 5.2.0?

I'm trying to access to sub components with nested routing in React using react-router-dom 5.2.0.
Here you can find a CodeSandbox link of the project: https://codesandbox.io/s/nested-routes-8c7wq?file=/src/App.js
First, let me show you the tree structure of my pages.
Home
About
Organisations
MainContent
SecondContent
Routes for main pages (Home, About, Organizations) are in src/routes folder.
So their link look like:
https://localhost:3000/ (for Home)
https://localhost:3000/about (for About)
https://localhost:3000/organizations (for Organizations)
At this point, everything is okay for me. The problem is with the nesting part..
On Organizations page, I have to be able to switch between MainContent and SecondContent.
So links into that page must look like this:
https://8c7wq.csb.app/organizations/maincontent
https://8c7wq.csb.app/organizations/secondcontent
But I cannot print any of those contents...
If you want to build nested paths you should use the path and url from the current route match and build the nested routes and links from there.
src/pages/Orgainizations
import { Switch, Route, useRouteMatch } from "react-router-dom";
const Organizations = (props) => {
const { path, url } = useRouteMatch(); // <-- get current path and url
const [value, setValue] = useState("");
const menuLinks = {
data: [
{
id: 0, // <-- fix React key issue in NavBar
title: "StackOverflow",
to: `${url}/maincontent` // <-- build nested link
},
{
id: 1, // <-- fix React key issue in NavBar
title: "BitBucket",
to: `${url}/secondcontent` // <-- build nested link
}
]
};
return (
<div>
<NavBar changeValue={(value) => setValue(value)} menuLinks={menuLinks} />
<div>
<Switch>
<Route
render={() => <MainContent title={value} />} // *
exact
path={`${path}/maincontent`} // <-- build nested path
strict
/>
<Route
render={() => <SecondContent title={value} />} // *
exact
path={`${path}/secondcontent`} // <-- build nested path
strict
/>
</Switch>
</div>
</div>
);
};
* Notice I converted to rendering route components via the render prop as opposed to the component prop. When you use anonymous functions to render a component it is created each render cycle, i.e. it will remount the component each time. The render prop does not do this. See render methods for more information.
NavBar
Fix the to prop value to render the link correctly, remove the leading "/" from the path and render the direct link.to value.
Change
to={`/${props.link.to}`}
to
to={props.link.to}
Demo

React/Redux/React Router - Only call API when route is first visited or the filter is changed

Is there a way to only run props.fetchTransactions() under the following conditions:
When the route is first navigated to
If props.filter is changed
At the moment, it runs every time the route is navigated to because <TransactionListContainer /> is unmounted and re-mounted by react router. This creates an unnecessary call to the API as it just re-fetches the exact same result set that is already in my Redux store.
The only way I can think of doing this is by lifting the useEffect hook up into the root <App/> so it's outside of the router, but i'm unsure if that's a good way to go because:
It will call the API when <App/> is loaded, even if the user is viewing a different route
<App/> will quickly become cluttered if I need to repeat this for other containers
// App root
const App = () => {
return (
<BrowserRouter>
<Switch>
<Route exact path="/" component={ () => <TransactionListContainer /> } />
...other routes...
</Switch>
</BrowserRouter>
);
}
// Container
const TransactionListContainer = (props) => {
useEffect(() => {
// This should only happen when the route is first visited or if props.filter has changed
props.fetchTransactions(props.filter.fields, props.filter.pagination);
},[props.filter]);
// The transactions are passed to this container
// by redux: props.transactions. I haven't included
// any code that I believe isn't relevant
// to the question.
}
You can provide a flag variable indicating if the transaction data was cached previously (is available in redux store).
const TransactionListContainer = (props) => {
useEffect(() => {
if(!props.isCached){
props.fetchTransactions(props.filter.fields, props.filter.pagination);
}
},[props.filter, props.isCached]);
}
function mapStateToProps(state) {
const isCached = state.transactions.length !== 0
return { isCached }
}
export default connect(mapStateToProps)(TransactionListContainer)

Dynamically render a react component with react routing, based on value given in url

Basically what I want to do is create a react portfolio project that contains and showcases all of my react projects. But I don't know how to render a project based on a url value.
What I mean is,
<Route path='/projects/:projectName' component={Project}></Route>
I want to render a component based on the :projectName vakue.
Or maybe create a Project component that just renders the given project based on the url value.
Is that even possible? I know I can use match to get the :projectName value, but how could I use it to render a component?
There are few approaches
1. As mentioned above to let project component decide what should be rendered based on match.params
const routes = {
'my-route1': <MyComponent1 />,
'my-route2': <MyComponent2 />
}
const Project = props => {
const { projectName } = props.match.params
return routes[projectName] || <DefaultComponent />
}
You may define your own routes components who will decide which component to Render based on state. It is helpful when you need to create master pages or templates and do not want any dependencies on match inside other components.
const PrivateRoute = ({ component: Component, ...rest }) => {
const func = props => (!!rest.isUserAllowedToNavigate()
? <Component {...props} />
: (
<Redirect to={
{
pathname: '/login',
search: props.location.pathname !== '/' && queryStringComposer({
redirect_from: props.location.pathname || getQueryStringParam('redirect_from')
})
}
}
/>
)
)
return (<Route {...rest} render={func} />)
}
/* Connecting to redux */
const PrivateRouteConnected = connect(mapStateToProps, mapDispatchToProps)(PrivateRoute)
/* Using as normal routes */
<PrivateRouteConnected exact path="/dashboard" component={Dashboard} />
your Project component can handle the logic to render a different component based on the URL param. For example:
const Project = props => {
const { projectName } = props.match.params
if (projectName === project1) {
return <ProjectOne addProps={addProps} />
}
if (projectName === project2) {
return <ProjectTwo />
}
return <DefaultProject />
}

Categories

Resources