Using client-only routes with page templates coming from Contentful - javascript

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.

Related

How to dynamically use react routing

I have a application where I have two logins one for superAdmin and other for 'Admin'.
I have several pages One which is common (home page) both users have excess to that.
then I have several other pages which are some for admin and others are for superAdmin.
Now when I open my page I am trying to go '/' this route (My home route).
What I am trying to do
Now If I am logged in as admin and admin user is typing some superAdmin url in address bar I want that to be redirect to current admin route
Same goes for superAdmin as well
both the user I want to restrict to excess each-others route
And if I am admin user or superAdmin user and trying to logged in and trying to excess authenticated route I should be redirect to home page
What I have done
I have created one component (Dynamic route) here I am checking what user is trying to do.
and in route.js in my routing file I am passing props as guest,superAdmin and admin
Dynamicroute.js code
I have created my context to store the user once they logged in
export default function Dynamicroute(props) {
const { user } = useAuthState(); // this I am getting from my context
console.log(user);
if (props.partner && !user) {
console.log('admin not logedin');
return <Redirect to="/admin" />;
} else if (props.admin && !user) {
console.log('superAdmin not loged in');
return <Redirect to="/superAdmin" />;
} else if (props.admin && user.role === 'admin') {
console.log('admin logedin');
return <Redirect to="/admin_home" />;
} else if (props.admin && user.role === 'superAdmin') {
console.log('super admin loged in');
return <Redirect to="/superadmin_home" />;
} else if (props.guest && user) {
console.log('guest');
return <Redirect to="/" />;
} else {
return <Route component={props.component} {...props} />;
}
}
My route.js
<DuynamicRoute exact path="/" component={Home} guest />
<DuynamicRoute path="/admin" component={loginAdmin} guest />
<DuynamicRoute path="/superAdmin" component={loginSuperAdmin} guest />
<DuynamicRoute path="/admin_home" component={admin_home} admin/>
<DuynamicRoute path="/superAdmin_home" component={superAdmin_home} superAdmin/>
Issue I am facing
I don't know what issue I am facing it is redirecting me to that route on login but content is not loading
If I console something on that page I am not able to get that, the page is getting blank.
I am following this lecture from 25:00 timing
Edited
Here is my code sand box
Please do check this
Edit
admin and super admin are going to be loged in different browsers, So just do not want admin to access super admin and vice-versa if they type in url each other's rout
For better management and development of the program along with the
best practices, Do the Authorization in React.js as follows:
Demo on Codesandbox
First: You need a class for checking permissions and routes/pages configs like below:
class AppUtils {
static setRoutes(config) {
let routes = [...config.routes];
if (config.auth) {
routes = routes.map((route) => {
let auth = config.auth ? [...config.auth] : null;
auth = route.auth ? [...auth, ...route.auth] : auth;
return {
...route,
auth
};
});
}
return [...routes];
}
static generateRoutesFromConfigs(configs) {
let allRoutes = [];
configs.forEach((config) => {
allRoutes = [...allRoutes, ...this.setRoutes(config)];
});
return allRoutes;
}
static hasPermission(authArr, userRole) {
/**
* If auth array is not defined
* Pass and allow
*/
if (authArr === null || authArr === undefined) {
// console.info("auth is null || undefined:", authArr);
return true;
} else if (authArr.length === 0) {
/**
* if auth array is empty means,
* allow only user role is guest (null or empty[])
*/
// console.info("auth is empty[]:", authArr);
return !userRole || userRole.length === 0;
} else {
/**
* Check if user has grants
*/
// console.info("auth arr:", authArr);
/*
Check if user role is array,
*/
if (userRole && Array.isArray(userRole)) {
return authArr.some((r) => userRole.indexOf(r) >= 0);
}
/*
Check if user role is string,
*/
return authArr.includes(userRole);
}
}
}
export default AppUtils;
Second: You need Authorization component for handling routes like below:
import React, { Component } from "react";
import AppUtils from "utils/AppUtils";
import { matchRoutes } from "react-router-config";
import { withRouter } from "react-router-dom";
import AppContext from "context/AppContext";
class AppAuthorization extends Component {
constructor(props, context) {
super(props);
const { routes } = context;
this.state = {
accessGranted: true,
routes
};
}
componentDidMount() {
if (!this.state.accessGranted) {
this.redirectRoute();
}
}
componentDidUpdate() {
if (!this.state.accessGranted) {
this.redirectRoute();
}
}
static getDerivedStateFromProps(props, state) {
const { location, userRole } = props;
const { pathname } = location;
const matched = matchRoutes(state.routes, pathname)[0];
return {
accessGranted: matched
? AppUtils.hasPermission(matched.route.auth, userRole)
: true
};
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.accessGranted !== this.state.accessGranted;
}
redirectRoute() {
const { location, userRole, history } = this.props;
const { pathname, state } = location;
const redirectUrl = state && state.redirectUrl ? state.redirectUrl : "/";
/*
User is guest
Redirect to Login Page
*/
if (!userRole || userRole.length === 0) {
history.push({
pathname: "/login",
state: { redirectUrl: pathname }
});
} else {
/*
User is member
User must be on unAuthorized page or just logged in
Redirect to dashboard or redirectUrl
*/
history.push({
pathname: redirectUrl
});
}
}
render() {
// console.info('App Authorization rendered', accessGranted);
return this.state.accessGranted ? (
<React.Fragment>{this.props.children}</React.Fragment>
) : null;
}
}
// AppAuthorization.defaultProps = {
// userRole: [] // You can manage roles by redux or any state managements
// };
AppAuthorization.contextType = AppContext;
export default withRouter(AppAuthorization);
Third: You need authRoles file or remote for managing roles on client like below:
/**
* Authorization Roles
*/
const authRoles = {
admin: ["admin"],
superAdmin: ["superAdmin"],
user: ["user"],
onlyGuest: []
};
export default authRoles;
Forth: If you want to move forward with this logic, you have to implement the structure of your pages as follows:
src
|---pages
|---home
|---HomePage.jsx
|---HomePageConfig.jsx
|
|- The rest of the pages should be implemented in the same way
For example: When you want to implement a page that only the admin can see (admin home page config):
import React from "react";
import authRoles from "auth/authRoles";
export const AdminHomePageConfig = {
auth: authRoles.admin,
routes: [
{
path: "/admin",
exact: true,
component: React.lazy(() => import("./HomePage"))
}
]
};
Or the home page that everyone can see:
import React from "react";
export const HomePageConfig = {
routes: [
{
path: "/",
exact: true,
component: React.lazy(() => import("./HomePage"))
}
]
};
According to the example above, you can enter the auth prop with the role here, to restrict access to the page.
To get a closer look at this logic, I implemented it in the Codesandbox, which you can see via the link below:
Demo on Codesandbox
Notice: The above demo needs to be more complete, and instead of storing user roles in the state, it is better to use state management
packages (redux, ...) and also perform login operations through
cookies.
Instead of creating a dynamic route, you can create a function that checks auth and redirects on entering the route.
const yourRouter = () => {
// yourAuthLogic
const routeAuth = (Component, props) => {
// redirect logic
// here you use the if/else branching based on auth state to redirect
// if no redirect
return (
<Component {...props} />
)
}
return (
<Router>
<Switch>
<Route path="/admin" render={() => routeAuth(component, props)} />
<Route path="/superAdmin" render={() => routeAuth(component, props)} />
</Switch>
</Router>
)
}
The problem is that the DuynamicRoute component returns a Redirect in its top level, but Redirects don't work directly inside a Switch components. This is because a Redirect in a Switch would lead to an infinite redirection loop.
To fix this, you should return a top level Route from your custom Route component, while handling the redirection logic between the Route tags.
Also, it's worth mentioning that some routes shouldn't be special protected routes, but regular landing pages, such as the home and login pages.
Here is a sample project based on your CodeSandbox solution:
https://codesandbox.io/s/vigilant-feather-jbq4j
I made it so that superAdmin user can access admin level, but not the other way around. A lesser admin cannot access superAdmin content, without changing the active user to a superAdmin.
Here is an additional link with a slight modification to logic, for the use case where you would prefer that admin and superAdmin can't access each-other's protected pages: https://codesandbox.io/s/brave-haze-zsmn9?file=/src/ProtectedRoute.js
As this doesn't have an accepted answer yet I'll throw in the simple approach I use for adding / removing routes based on a condition.
The examples use Typescript but it should be relatively easy to strip the types out if needed.
Conditional Route Component:
This component has full typing of all the Route props plus an added prop called enabled.
When enabled is true the route will be rendered as normal, when it is false null will be returned.
// ConditionalRoute.tsx
import * as React from 'react';
import { Route, RouteProps } from 'react-router-dom';
interface ConditionalRouteProps extends RouteProps {
enabled?: boolean;
}
const ConditionalRoute: React.FunctionComponent<ConditionalRouteProps> = ({
enabled,
...routeProps
}) => {
if (!enabled) {
return null;
}
return (
<Route {...routeProps} />
);
};
export default ConditionalRoute;
Conditional Redirect Component:
This component has full typing of all the Redirect props plus an added prop called enabled.
When enabled is true the redirect will be actioned as normal, when it is false null will be returned.
// ConditionalRedirect.tsx
import * as React from 'react';
import { Redirect, RedirectProps } from 'react-router-dom';
interface ConditionalRedirectProps extends RedirectProps {
enabled?: boolean;
}
const ConditionalRedirect: React.FunctionComponent<ConditionalRedirectProps> = ({
enabled,
...redirectProps
}) => {
if (!enabled) {
return null;
}
return (
<Redirect {...redirectProps} />
);
};
export default ConditionalRedirect;
Using the Conditional Components:
Use the Conditional Routes / Redirects as you would their original base components except they will not come into effect unless the enabled property is true.
This also works with the Switch component as it will ignore components that return null.
// App.tsx
import * as React from 'react';
import { BrowserRouter, Switch } from 'react-router-dom';
interface AppProps {
authenticatedUser?: any;
}
const ConditionalRedirect: React.FunctionComponent<AppProps> = ({
authenticatedUser
}) => {
return (
<BrowserRouter>
<Switch>
{
// Always allow the unauthenticated error route
}
<Route
path="/error/401"
component={Unauthenticated}
/>
{
// If user is authenticated allow member route
}
<ConditionalRoute
enabled={!!authenticatedUser}
path="/member"
component={MemberHome}
/>
{
// If user is an admin allow admin route
}
<ConditionalRoute
enabled={authenticatedUser?.isAdmin}
path="/administration" component={AdminHome}
/>
{
// If user is an admin redirect uncaught route
// to administration route
}
<ConditionalRedirect
enabled={authenticatedUser?.isAdmin}
path="/"
to="/administration"
/>
{
// If user is authenticated redirect uncaught route
// to member route
}
<ConditionalRedirect
enabled={!!authenticatedUser}
path="/"
to="/member"
/>
{
// If user is not authenticated redirect uncaught
// route to unauthenticated error route
}
<ConditionalRedirect
enabled={!authenticatedUser}
path="/"
to="/error/401"
/>
</Switch>
</BrowserRouter>
);
};
export default App;
The main caveat of this solution is that any value in a condition has to be ready to be evaluated before rendering any of the routes. For example if you make an async request to check if a user is authenticated in the example App above, when the user is initially undefined before the async request returns it will redirect you to the 401 page.

How to implement routing from and to root (index.js) in react with redux

I'm programming a react server webpage, trying to redirect from index.js (i.e: localhost:3000) to Login page: (localhost:3000/login), and from login to index (in case of failed login). What do I need to write in index.js and login.js?
This is for a react based app, using also redux framework. I've tried a few ways including setting up a BrowserRouter etc. All won't really do the redirecting.
My current code is this:
in index.js:
class Index extends Component {
static getInitialProps({store, isServer, pathname, query}) {
}
render() {
console.log(this.props);
//const hist = createMemoryHistory();
if (!this.props.isLoggedIn){
return(<Switch><Route exact path = "/login"/></Switch>)
}
else{...}
in login.js:
render() {
console.log(this.props);
if (fire.auth().currentUser != null) {
var db = fire.firestore();
db.collection("users").doc(fire.auth().currentUser.uid).get().then((doc) => {
this.props.dispatch({ type: 'LOGIN', user: doc.data() });
})
}
const { isLoggedIn } = this.props
console.log(isLoggedIn)
if (isLoggedIn) return <Redirect to='/' />
I except the root to redirect to login if no session is on, and login to redirect to root once there is a successful login.
I am currently getting "You should not use <Switch> outside a <Router>" at index (I have tried to wrap with BrowserRouter, ServerRouter, Router. the first says it needs DOM history. adding history does not change error. two others do not error but are blank display on browser.)
and "ReferenceError: Redirect is not defined" at login.
Any help will be appreciated.
you can use a HOC (Higher-Order Components)
something like this
export default ChildComponent => {
class ComposedComponent extends Component {
componentWillMount() {
this.shouldNavigateAway();
}
componentWillUpdate() {
this.shouldNavigateAway();
}
shouldNavigateAway() {
if (!this.props.authenticated) {
this.props.history.push('/')
}
}
render() {
return <ChildComponent {...this.props} />
}
}
}
As of now you're trying to return a route declaration wrapped in a Switch component. If you want to redirect the user to the /login page if hes not logged in, you need the route to be declared higher up in the component hierarchy, and then you would be able to return the <Redirect /> component. Either way, I would suggest you check out the react router documentation to see how they do authentication.
https://reacttraining.com/react-router/web/example/auth-workflow

How to get previous url in react gatsby

I am pretty much familiar with the React.js but new to Gatsby.
I want to detect the previous page URL in Gatsby?
You can pass down state using the Link component:
import React from 'react';
import { Link } from 'gatsby';
const PrevPage = () => (
<div>
<Link
to={`/nextpage`}
state={{ prevPath: location.pathname }}
>
Next Page
</Link>
</div>
)
const NextPage = (props) => (
<div>
<p>previous path is: {props.location.state.prevPath}</p>
</div>
);
Then you have access to prevPath from this.props.location.state in the next page.
Full credit to #soroushchehresa's answer — this answer is just extras built upon it.
Gatsby will throw error during production build, since location is not available during server-side rendering. You could get around it by checking for window object first:
class Page extends React.Component {
state = {
currentUrl: '',
}
componentDidMount() {
if (typeof window == 'undefined') return
this.setState({ currentUrl: window.location.href })
}
render() {
return (
<Link to="..." state={{ prevUrl: this.state.currentUrl }}>
)
}
}
But this requires us to implement this on every page, which is tedious. Gatsby has already set up #reach/router for server-side rendering, so we can hook into its location props. Only router components get that props, but we can use #reach/router's Location component to pass it to other components.
With that, we can write a custom Link component that always pass previous url in its state:
// ./src/components/link-with-prev-url.js
import React from 'react'
import { Location } from '#reach/router'
import { Link } from 'gatsby'
const LinkWithPrevUrl = ({ children, state, ...rest }) => (
<Location>
{({ location }) => (
//make sure user's state is not overwritten
<Link {...rest} state={{ prevUrl: location.href, ...state}}>
{ children }
</Link>
)}
</Location>
)
export { LinkWithPrevUrl as Link }
Then we can import our custom Link component instead of Gatsby's Link:
- import { Link } from 'gatsby'
+ import { Link } from './link-with-prev-url'
Now each Gatsby page component will get this previous url props:
const SomePage = ({ location }) => (
<div>previous path is {location.state.prevUrl}</div>
);
You might also consider creating a container that store state for the client side & use the wrapRootElement or wrapPageElement in both gatsby-ssr.js and gatsby-browser.js.
These answers are partially correct. If you set state using link api then the state persists in browser history.
So if you go from Page1 to Page2 then the eg the state.prevUrl will correctly be set to Page1
But if the you go to Page3 from Page2 and then do a browser back then the state.prevUrl will still be Page1 which is false.
Best way I found to deal with this is to add something like this on the gatsby-browser.js
export const onRouteUpdate = ({ location, prevLocation }) => {
if (location && location.state)
location.state.referrer = prevLocation ? prevLocation.pathname : null
}
this way you will have always the previous url available on location.
I resolved my problem with the below piece of code. Here is the ref link https://github.com/gatsbyjs/gatsby/issues/10410
// gatsby-browser.js
exports.onRouteUpdate = () => {
window.locations = window.locations || [document.referrer]
locations.push(window.location.href)
window.previousPath = locations[locations.length - 2]
}
Now you can get previousPath can be accessed from anywhere.

How to print another component when i logout. React and Firebase

How I can render another component when i logout on firebase.
I´m trying to re-render the page to print the LoginPage
This is my LoginPage that is render when I loggin with another form.
import React, { Component } from "react";
/*Importing firebase*/
import firebase from "firebase";
/*Importing pages*/
import LoginPage from "../login/LoginPage";
/*Importing components*/
import Header from "./containers/HandleHeader";
class IndexPage extends Component {
shouldComponentUpdate() {
firebase.auth().onAuthStateChanged(user => {
if (user) {
} else {
this.forceUpdate();
return <LoginPage />;
}
});
}
render() {
return (
<div>
<Header />
</div>
);
}
}
export default IndexPage;
And this is my handleLogout that work when I click my logout button.
handleLogout = e => {
firebase
.auth()
.signOut()
.then(() => this.forceUpdate());
};
I want to make that when I logout I don´t need reload the page.
Usually the best way to do this is to maintain the logged-in state somewhere, then protect the entry points to any components that require authentication with logic like this:
render() {
const { loggedIn } = this.props;
if (!loggedIn) return <Redirect to="/login" />;
// Reset of component rendered below this point
}
Note that this logic can be in the component itself, some parent component, or some higher order component. The key is to have it somewhere that will prevent access to any protected component by redirecting in the render method before any protected information can be reached.
Redirecting is often achieved using some routing package like, say, react-router-dom, to navigate around. This means that when you log out, a user is implicitly always redirected because they can no longer access the protected components anymore.

React event listeners based on changes in state made with Redux

Good afternoon,
I am having some difficulty working with React and Redux when I am trying to redirect users of my app based on changes in state.
At a high level: I want my app's route to change when my user object in state is populated with information from home / to /student/:username.
Right now I have accomplished this in a hacky sort of fashion.
In my Login component I use the componentDidUpdate lifecycle hook to listen and dispatch an action when an access token from a 3rd party API is passed back to the client from my Express server.
import React from "react";
import LoginForm from "../minor/LoginForm";
const Login = React.createClass({
componentDidUpdate(){
const {success, access_token} = this.props.loginStatus;
if (success === true && access_token !== null){
console.log("It worked getting your data now...");
this.props.getUserData(access_token);
} else if (success === false || access_token === null){
console.log("Something went wrong...");
}
},
render(){
return(
<div className="loginComponentWrapper">
<h1>Slots</h1>
<LoginForm loginSubmit={this.props.loginSubmit}
router={this.props.router}
user={this.props.user} />
New User?
</div>
)
}
});
Notice that I am passing router and user to my LoginForm component. I do this in order to use ANOTHER componentDidUpdate where I use the .push method on router like so:
import React from "react";
const LoginForm = React.createClass({
componentDidUpdate(){
const {router, user} = this.props;
if (user.username !== null){
router.push(`/student/${user.username}`);
}
},
render(){
return(
<div className="loginWrapper">
<div className="loginBox">
<form className="loginForm" action="">
<input ref={(input) => this.username_field = input} type="text" placeholder="username" defaultValue="kitties#kit.com" />
<input ref={(input) => this.password_field = input} type="text" placeholder="password" defaultValue="meowMeow3" />
<button onClick={this.loginAttempt}>Login</button>
</form>
</div>
</div>
)
}
});
Sure it works but I'm certain this is a overly complicated solution and not what is considered a best practice. I've tried adding a custom listener method within my Login component but I've never had it successfully fire off, instead my app gets stuck in a loop.
Is there a way I can do this using Redux and keep my components tidier, or take advantage of componentDidUpdate in a more efficient way?
As always I'd appreciate some more experienced eyes on my issue and look forward to some feedback!
Thanks
UPDATE
App.js
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import * as actionCreators from "../actions/userActions.js";
import StoreShell from "./StoreShell.js";
function mapStateToProps(state){
return{
loginStatus: state.loginStatus,
user: state.user
}
}
function mapDispatchToProps(dispatch){
return bindActionCreators(actionCreators, dispatch)
}
const App = connect(mapStateToProps, mapDispatchToProps)(StoreShell);
export default App;
This component "sprinkles" all my redux stuff and state data into my container component named StoreShell that in turn passes all that data to props for the elements that make up the UI like Login and LoginForm am I taking too many steps?
StoreShell.js
import React from "react";
const StoreShell = React.createClass({
render(){
return(
<div className="theBigWrapper">
{React.cloneElement(this.props.children, this.props)}
</div>
)
}
})
export default StoreShell;
There are several things that could make the login flow easier to manage and reason about and tidy up your components a bit. First a few general points:
I'm not certain why you have divided login logic between the two components? The idiomatic React/Redux approach would be to have a container component that deals with logic, and a presentation component that deals with presentation. Currently both components do a little bit of each.
You don't need to pass props.router up and down through your components. React router provides a HOC that provides router as a props called withRouter( docs here). You just wrap a component in withRouter and props.router is available - everything else stays the same.
export default withRouter(LoginForm);
My personal feeling is that the URI is part of your app's state, and as such it should be maintained within the store and updated by dispatching actions. There is a cracking library available to do this - react-router-redux. Once you have it setup then you can pass the push method to your components (or elsewhere... see the next points) to navigate:
import { push } from 'react-router-redux';
import { connect } from 'react-redux';
const NavigatingComponent = props => (
<button onClick={() => props.push('/page')}>Navigate</button>
);
const mapStateToProps = null;
const mapDispatchToProps = {
push
};
export default connect(mapStateToProps, mapDispatchToProps)(NavigatingComponent);
One of the great things about having the URI in our store is that we don't need access to props.router to change location, which opens the avenue of moving the login logic outside of our components. There are several ways we can do this, the simplest is probably redux-thunk, which allows our action creators to return functions instead of objects. We could then write our component to simply call a login function with username and password entered, and our thunk takes care of the rest:
import { push } from 'react-router-redux';
// action creators....
const loginStarted = () => ({
type: 'LOGIN_STARTED'
)};
const loginFailed = (error) => ({
type: 'LOGIN_FAILED',
payload: {
error
}
};
const loginSucceeded = (user) => ({
type: 'LOGIN_SUCCEEDED',
payload: {
user
}
};
const getUserData = (access_token) => (dispatch) => {
Api.getUserData(access_token) // however you get user data
.then(response => {
dispatch(loginSucceeded(response.user));
dispatch(push(`/student/${response.user.username}`));
});
export const login = (username, password) => (dispatch) => {
dispatch(loginStarted());
Api.login({ username, password }) // however you call your backend
.then(response => {
if (response.success && response.access_token) {
getUserData(access_token)(dispatch);
} else {
dispatch(loginFailed(response.error));
}
});
}
The only thing your components do is call the initial login function, which could be implemented something like:
Container:
import React from 'react';
import { connect } from 'react-redux';
import { login } from '../path/to/actionCreators';
import LoginForm from './LoginForm';
const LoginContainer = React.createClass({
handleSubmit() {
this.props.login(
this.usernameInput.value,
this.passwordInput.value
);
},
setUsernameRef(input) {
this.usernameInput = input;
},
setPasswordRef(input) {
this.passwordInput = input;
},
render() {
return (
<LoginForm
handleSubmit={this.handleSubmit.bind(this)}
setUsernameRef={this.setUsernameRef.bind(this)}
setPasswordRef={this.setPasswordRef.bind(this)}
/>
);
}
});
const mapStateToProps = null;
const mapDispatchToProps = {
login
};
export default connect(mapStateToProps, mapDispatchToProps)(LoginContainer);
Component:
import React from 'react';
export const LoginForm = ({ handleSubmit, setUsernameRef, setPasswordRef }) => (
<div className="loginWrapper">
<div className="loginBox">
<form className="loginForm" action="">
<input ref={setUsernameRef} type="text" placeholder="username" defaultValue="kitties#kit.com" />
<input ref={setPasswordRef} type="text" placeholder="password" defaultValue="meowMeow3" />
<button onClick={handleSubmit}>Login</button>
</form>
</div>
</div>
);
This has achieved separation of a logic/data providing container, and a stateless presentational component. The LoginForm component can be written simply as a function above because it has no responsibility to deal with logic. The container is also a very simple component - the logic has all been isolated in our action creator/thunk and is much easier to read and reason about.
redux-thunk is just one way of managing asynchronous side effects with redux - there are many others with different approaches. My personal preference is toward redux-saga, which may be interesting for you to look at. In my own redux journey, however, I certainly benefited from using and understanding redux-thunk first before finding it's limitations/drawbacks and moving on, and would suggest this route to others.
If you're using react-router version 4.x.x: You can just render a Redirect component that handles the redirection for you (see example in react-router docs).
import React from "react";
import { Redirect } from "react-router";
import LoginForm from "../minor/LoginForm";
const Login = React.createClass({
componentDidUpdate(){
const {success, access_token} = this.props.loginStatus;
if (success === true && access_token !== null){
console.log("It worked getting your data now...");
this.props.getUserData(access_token);
} else if (success === false || access_token === null){
console.log("Something went wrong...");
}
}
render(){
// if user is defined, redirect to /student/:username
if (this.props.user.username !== null) {
return (<Redirect to={ `/student/${this.props.user.username}` } />)
}
// otherwise render the login form
return(
<div className="loginComponentWrapper">
<h1>Slots</h1>
<LoginForm loginSubmit={this.props.loginSubmit}
router={this.props.router}
user={this.props.user} />
New User?
</div>
)
}
});
If you're using react-router version 3.x.x: The the way you're doing it is mostly correct. I would move the redirect from the LoginForm component to Login component; that way LoginForm does not need to depend on the user prop.
I know, I don't like the pattern much either.
Hope this helps!

Categories

Resources