What is best option to guard your routes in React? - javascript

I am using this HOC to guard my routes but I find odd using this HOC in every component because I am already using 1 or 2 HOC's like reduxForm etc
import React from "react";
import { connect } from "react-redux";
export default ChildComponent => {
class ComposedComponent extends React.Component {
componentDidMount() {
this.shouldNavigateAway();
}
componentDidUpdate() {
this.shouldNavigateAway();
}
shouldNavigateAway() {
if (!this.props.auth) {
this.props.history.push("/");
}
}
render() {
return <ChildComponent {...this.props} />;
}
}
const mapStateToProps = state => {
return { auth: state.auth };
};
return connect(mapStateToProps)(ComposedComponent);
};

The HoC approach is right, but you should apply it to routes, not components.
Take a look at the pattern used in redux-auth-wrapper

I do not know how you implement your routes but there is clean solution for this.
render() {
let routes = (
<Switch>
<Route path="/auth" component={asyncAuth} />
<Route path="/" exact component={BurgerBuilder} />
<Redirect to="/" />
</Switch>
);
if (this.props.isAuthenticated) {
routes = (
<Switch>
<Route path="/checkout" component={asyncCheckout} />
<Route path="/orders" component={asyncOrders} />
<Route path="/logout" component={Logout} />
<Route path="/auth" component={asyncAuth} />
<Route path="/" exact component={BurgerBuilder} />
<Redirect to="/" />
</Switch>
);
}
return (
<div>
<Layout>
{routes}
</Layout>
</div>
);
}
And store the auth token in your redux.
const mapStateToProps = state => {
return {
isAuthenticated: state.auth.token !== null
};
};

Related

Why am I getting "Objects are not valid as a React child" error?

I have a lot of files, but I think the problem is coming from my authentication component in React. I basically want to only display a page if the user is logged in otherwise I want to the user to be redirected.
react-dom.development.js:14887 Uncaught Error: Objects are not valid as a React child (found: object with keys {$$typeof, type, compare, WrappedComponent}). If you meant to render a collection of children, use an array instead.
requireAuth.js
// function that can wrap any component to determine if it is authenticated
import React, { Component } from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { push } from "#lagunovsky/redux-react-router"; // not sure if this correct
export default function requireAuth(Component) {
class AuthenticationComponent extends React.Component {
constructor(props) {
super(props);
this.checkAuth();
}
componentDidUpdate(prevProps, prevState) {
this.checkAuth();
}
checkAuth() {
// if not authenticated then it is redirected
if (!this.props.isAuthenticated) {
const redirectAfterLogin = this.props.location.pathname;
this.props.dispatch(push(`/login?next=${redirectAfterLogin}`));
}
}
// if authenticated then renders the component
render() {
return (
<div>
{this.props.isAuthenticated === true ? (
<Component {...this.props} />
) : null}
</div>
);
}
}
AuthenticationComponent.propTypes = {
isAuthenticated: PropTypes.bool.isRequired,
location: PropTypes.shape({
pathname: PropTypes.string.isRequired,
}).isRequired,
dispatch: PropTypes.func.isRequired,
};
// checks isAuthenticated from the auth store
const mapStateToProps = (state) => {
return {
isAuthenticated: state.auth.isAuthenticated,
token: state.auth.token,
};
};
return connect(mapStateToProps)(AuthenticationComponent);
}
App.js
class App extends Component {
render() {
return (
<div>
<Root>
<ToastContainer hideProgressBar={true} newestOnTop={true} />
<Routes>
<Route exact path="/" element={<Home />} />
<Route path="/closet" element={requireAuth(Closet)} />
<Route path="*" element={<NotFound />} />
</Routes>
</Root>
</div>
);
}
}
I have done some digging but I can't find a problem like this.
The error is because on this line:
<Route path="/closet" element={React.createComponent(requireAuth(Closet))} />
You're passing the actual class definition to the element prop and not an instance of the class (which would be the React component). To fix this, you can use React.createElement:
class App extends Component {
render() {
return (
<div>
<Root>
<ToastContainer hideProgressBar={true} newestOnTop={true} />
<Routes>
<Route exact path="/" element={<Home />} />
<Route path="/closet" element={React.createElement(requireAuth(Closet))} />
<Route path="*" element={<NotFound />} />
</Routes>
</Root>
</div>
);
}
}
Because Route's element props need a ReactNode,But requireAuth(Closet)'s type is () => React.ReactNode, you can change your App.js like this:
const AuthComponent = requireAuth(Closet);
class App extends Component {
render() {
return (
<div>
<Root>
<ToastContainer hideProgressBar={true} newestOnTop={true} />
<Routes>
<Route exact path="/" element={<Home />} />
<Route path="/closet" element={<AuthComponent />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Root>
</div>
);
}
}

Always redirected to current route instead of desired route

I am using ternary operator to render component using react route. But I am always redirected to "/" route instead of desired component even if the condition is as expected.
I get desired functionality only when I refresh/reload the page.
This is my App.js with all the routes
import React, { useEffect, useState } from 'react';
import Header from './Components/Header';
import Home from './Components/Home';
import Checkout from "./Components/Checkout";
import Payment from "./Components/Payment";
import NewProduct from "./Components/NewProduct";
import OrderSuccess from "./Components/OrderSuccess";
import AddressForm from './Components/AddressForm';
import {BrowserRouter, Switch, Route} from "react-router-dom";
import { Redirect } from 'react-router';
import Login from './Components/Login';
import Orders from "./Components/Orders";
import Account from './Components/Account';
import { connect } from 'react-redux';
const App=()=>{
const user = (JSON.parse(localStorage.getItem("profile")));
const defaultRoutes= ()=>{
return(
<div>
<Header/>
<Switch>
<Route path="/account-settings" exact component={()=>user ? <Account />: <Redirect to="/" />} />
<Route path="/orders" exact component={()=>user ? <Orders /> : <Redirect to="/" />} />
<Route path="/checkout" exact component={()=>user ? <Checkout />: <Redirect to="/" />} />
<Route path="/payment" exact component={()=>user ? <Payment /> : <Redirect to="/" />} />
<Route path="/account-settings/add-new-address" exact component={()=>user?.result ? <AddressForm /> : <Redirect to="/" />} />
<Route path="/" exact component={Home} />
</Switch>
</div>
)
}
return (
<BrowserRouter>
<Switch>
<Route path="/login" exact component={()=>user ? <Redirect to="/" />:<Login />} />
<Route component={defaultRoutes} />
</Switch>
</BrowserRouter>
);
}
export default App;
This is my index.js file
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Provider } from "react-redux";
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import "./index.css"
import reducers from "./reducers/index";
const store = createStore(reducers,{},compose(applyMiddleware(thunk)));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById("root")
)
This is reducer where I am updating my localstorage
export default (state={authData:null},action)=>{
switch(action.type){
case "AUTH":
localStorage.setItem("profile",JSON.stringify({...action?.data}));
return {...state, authData: action.data, loading: false, errors: null};
case "LOGOUT":
localStorage.clear();
return {...state,authData:null};
default:
return "";
}
}
Remove the ternary in the router. Use useHistory to redirect to "/" if the condition is not met.
For checking the condition, create a custom hook called useCheckUser which checks if there is a user in the localStorage.
In every component where user is required,
import useCheckUser from "file-address"
import { useHistory } from "react-router"
const Component = () => {
let history = useHistory();
let user = useCheckUser();
if (!user) {
history.push("/");
}
return ()
}
The useCheckUser hook should be something similar to
const useCheckUser = () => {
let user = localStorage.getItem("profile");
if (user) {
return JSON.parse(user);
}
return null;
};
export default useCheckUser;
NOTE: If your only requirement is rendering components when the user is present, you can change your custom hook so that it does everything including the redirecting. This is just to return the current user. So this can be used if you want to render certain components like auth pages if and only if the user is not present.
try to take the user a State (useState), and set it in the useEffect like this:
const [user, setUser] = useState(null);
useEffect(() => {
setUser(localStorage.getItem("profile"))
}, [])
it will set the "user" as your page rendered
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
setUser(localStorage.getItem("profile"))
}, [])
const authentication = {
getLogInStatus() {
return user;
},
};
}
function SecuredRoute(props) {
return (
<Route
path={props.path}
render={(data) =>
authentication.getLogInStatus() ? (
<props.component
{...data}
></props.component>
) : (
handleRedirect()
)
}
></Route>
);
}
const handleRedirect = () => {
let login = window.confirm(
"please Register or log in to access this page!"
);
if (login) {
return <Redirect path="/"></Redirect>;
}
};
return (
<BrowserRouter>
<Switch>
// those routes which don't needs authetication
<Route path="/home" compponent={Home} />
<Route path="/login" compponent={Login} />
//and others
// those routes which needs authetication
<SecuredRoute path="/bookings" component={Bookings} />
<SecuredRoute
path="/booking-details"
component={BookingDetails}
/>
<SecuredRoute path="/favorites" component={Favorites} />
<SecuredRoute path="/profile" component={Profile} />
</Switch>
</BrowserRouter>
);
}
Issue
You are close. The issue here is that even though you store the auth in localStorage and update the redux state, App isn't being rerendered to "pick up" the new auth value from localStorage.
Solution
Initialize the redux state from localStorage.
Select the authentication state from your Redux store. This is so changes to the store trigger subscribed components to rerender.
Auth Reducer
const initialState={
authData:JSON.parse(localStorage.getItem("profile")),
loading:false,
errors:null
};
export default (state = initialState, action) => {
switch(action.type){
case "AUTH":
localStorage.setItem("profile", JSON.stringify({ ...action?.data }));
return {
...state,
authData: action?.data,
loading: false,
errors: null
};
case "LOGOUT":
localStorage.clear();
return { ...state, authData: null };
default:
return state;
}
};
App
import { useSelector } from "react-redux";
const App = () => {
const user = useSelector(state => state.auth.authData);
const defaultRoutes = () => {
return (
<div>
<Header/>
<Switch>
<Route
path="/account-settings"
render={() => user ? <Account />: <Redirect to="/" />}
/>
<Route
path="/orders"
render={() => user ? <Orders /> : <Redirect to="/" />}
/>
<Route
path="/checkout"
render={() => user ? <Checkout />: <Redirect to="/" />}
/>
<Route
path="/payment"
render={() => user ? <Payment /> : <Redirect to="/" />}
/>
<Route
path="/account-settings/add-new-address"
render={() => user?.result ? <AddressForm /> : <Redirect to="/" />}
/>
<Route path="/" component={Home} />
</Switch>
</div>
)
};
return (
<BrowserRouter>
...
</BrowserRouter>
);
}
Suggestion
Review the auth workflow and create a PrivateRoute component that handles the redirect for you.
Example:
const PrivateRoute = props => {
const user = useSelector(state => state.auth.authData);
return user ? <Route {...props} /> : <Redirect to="/" />
};
Usage:
const App = () => {
const defaultRoutes = () => {
return (
<div>
<Header/>
<Switch>
<PrivateRoute path="/account-settings" component={Account} />
<PrivateRoute path="/orders" component={Orders} />
<PrivateRoute path="/checkout" component={Checkout} />
<PrivateRoute path="/payment" component={Payment} />
<Route
path="/account-settings/add-new-address"
render={() => user?.result ? <AddressForm /> : <Redirect to="/" />}
/>
<Route path="/" component={Home} />
</Switch>
</div>
)
};
return (
<BrowserRouter>
...
</BrowserRouter>
);
}

Pass Props in a Private Route React

I'm trying to pass several props in a private route. What's the correct way to write this and what am I missing? Here is the code I have. My app works with this code, in that the user is able to login and see the dashboard. However, the props aren't passing. Is there a way to pass props to a private route?
<PrivateRoute exact path="/dashboard" component={Dashboard} render={routeProps =>
<Dashboard
handleUpdate={this.handleUpdate}
book={this.state.book}
info={this.state.info}
{...routeProps} />}
/>
Dashboard Component
class Dashboard extends Component {
state = {
book: this.props.book,
info: this.props.info,
error: '',
}
onLogoutClick = e => {
e.preventDefault();
this.props.logoutUser();
};
render() {
console.log(`BOOK STATE IN DB: ${this.state.book}`)
const { user } = this.props.auth;
return(
<div>
<h4>
<b>This is your page</b> {user.name}
</h4>
<button onClick={this.onLogoutClick}>Logout</button>
<h2>Search Book</h2>
<Search
handleUpdate={this.props.handleUpdate}
/>
<h4>Book Results</h4>
<div>{this.state.book}</div>
</div>
);
}
}
Dashboard.propTypes = {
logoutUser: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(
mapStateToProps,
{ logoutUser }
)(Dashboard);
Private Route
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { connect } from "react-redux";
import PropTypes from "prop-types";
const PrivateRoute = ({ component: Component, auth, ...rest }) => (
console.log(auth),
<Route
{...rest}
render={props =>
auth.isAuthenticated === false ? (
<Redirect to="/login" />
) : (
<Component {...props} />
)
}
/>
);
PrivateRoute.propTypes = {
auth: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps)(PrivateRoute);
Can you show us the code of PrivateRouter component? You can just follow the such way
<PrivateRoute exact path="/dashboard" component={Dashboard} props = {{book: this.state.book etc}}/>
And receive this props on PrivateRoute components to put it into child component
Can you try removing the component={Dashboard} prop, and only use the render prop to render the Dashboard. Your code should look like this
<PrivateRoute exact path="/dashboard" render={routeProps =>
<Dashboard
handleUpdate={this.handleUpdate}
book={this.state.book}
info={this.state.info}
{...routeProps} />}
/>
From the docs
Warning: <Route component> takes precedence over <Route render> so don’t use both in the same .
So, remove the component={Dashboard}
After the comments and the PrivateRoute code, i suggest you rewrite your PrivateRoute to
const PrivateRoute = ({ auth, ...rest }) => {
if (!auth.isAuthenticated) {
return <Redirect to="/login" />;
}
return <Route {...rest} />
);
and remove the component={Dashboard} part.
const PrivateRoute = ({component: Component, auth, book, handleUpdate, ...rest }) => (
console.log(rest),
console.log(book),
<Route
{...rest}
render={props =>
auth.isAuthenticated === false ? (
<Redirect to="/login" />
) : (
<Component book={book} handleUpdate={handleUpdate} {...props} />
)
}
/>
)

React Router rendering blank pages with React Redux

In my App.js, I have the following:
const Index = asyncRoute(() => import('~/pages/index'))
const Register = asyncRoute(() => import('~/pages/register'))
const AddDesign = asyncRoute(() => import('~/pages/add-design'))
const Login = asyncRoute(() => import('~/pages/login'))
class App extends Component {
render() {
const { isLoggedIn } = this.props;
if(!isLoggedIn){
return (
<Switch>
<Route path={'/login'} component={Login} />
<Route path={'/register'} component={Register} />
<Redirect to={'/login'} />
</Switch>
);
}
return (
<Switch>
<Route exact path='/' component={Index}/>
<Route exact path='/add-design' component={AddDesign}/>
<Route exact path="/login" render={() => <Redirect to="/"/>} />
<Route exact path="/register" render={() => <Redirect to="/"/>} />
<Redirect to={'/'} />
</Switch>
);
}
}
function mapStateToProps({ user }) {
return {
isLoggedIn: !!user.token,
};
}
export default connect(mapStateToProps)(App);
When the user logs in, isLoggedIn is set to true and it then attempts to redirect the user back to "/"
This happens, however the page loaded is the index.html file within public, rather than the Index component.
I'm not sure if its making a difference, but my asyncRoute is a workaround for FOUC:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Loading from '~/components/Loading'
class AsyncImport extends Component {
static propTypes = {
load: PropTypes.func.isRequired,
children: PropTypes.node.isRequired
}
state = {
component: null
}
_hasClass(target, className) {
return new RegExp('(\\s|^)' + className + '(\\s|$)').test(target.className);
}
toggleFoucClass () {
const root = document.getElementById('root')
if (this._hasClass(root, 'fouc')) {
root.classList.remove('fouc')
} else {
root.classList.add('fouc');
}
}
componentWillMount () {
this.toggleFoucClass()
}
componentDidMount () {
this.props.load()
.then((component) => {
setTimeout(() => this.toggleFoucClass(), 1)
this.setState(() => ({
component: component.default
}))
})
}
render () {
return this.props.children(this.state.component)
}
}
const asyncRoute = (importFunc) =>
(props) => (
<AsyncImport load={importFunc}>
{(Component) => {
return Component === null
? <Loading size="large" />
: <Component {...props} />
}}
</AsyncImport>
)
export default asyncRoute
Can anyone explain why my users are being routed but the component not rendering?
Assuming using RR4
Try:
<Route path="/" component={App}>
<IndexRedirect to="/index" />
<Route path="index" component={Index} />
Refer to the following to get your usecase correct:
https://github.com/ReactTraining/react-router/blob/5e69b23a369b7dbcb9afc6cdca9bf2dcf07ad432/docs/guides/IndexRoutes.md

React Router Redirect drops param

I am using the next version of React Router, and it seems to be dropping params. I expect the redirect below to retain the value of channelId, but the to route uses the literal string ":channelId" in the path instead.
<Switch>
<Route exact path="/" component={Landing} />
<Route path="/channels/:channelId/modes/:modeId" component={Window} />
<Redirect
from="/channels/:channelId"
to="/channels/:channelId/modes/window" />
</Switch>
This looks like a resolved issue, but it's not working. Is there something else I need to pass to the to route?
Here's what I've been using, similar to the other answer but without a dependency:
<Route
exact
path="/:id"
render={props => (
<Redirect to={`foo/${props.match.params.id}/bar`} />;
)}
/>
I found no such logic in React Router 4 sources, so write own workaround:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import pathToRegexp from 'path-to-regexp';
import { Route, Redirect } from 'react-router-dom';
class RedirectWithParams extends Component {
render() {
const { exact, from } = this.props;
return (
<Route
exact={exact}
path={from}
component={this.getRedirectComponent}
/>);
}
getRedirectComponent = ({ match: { params } }) => {
const { push, to } = this.props;
const pathTo = pathToRegexp.compile(to);
return <Redirect to={pathTo(params)} push={push} />
}
};
RedirectWithParams.propTypes = {
exact: PropTypes.bool,
from: PropTypes.string,
to: PropTypes.string.isRequired,
push: PropTypes.bool
};
export default RedirectWithParams;
usage example:
<Switch>
<RedirectWithParams
exact from={'/resuorce/:id/section'}
to={'/otherResuorce/:id/section'}
/>
</Switch>
You can do this:
<Switch>
<Route exact path="/" component={Landing} />
<Route path="/channels/:channelId/modes/:modeId" component={Window} />
<Route
exact
path="/channels/:channelId"
render={({ match }) => (
<Redirect to={`/channels/${match.params.channelId}/modes/window`} />
)}
/>
</Switch>
I did this, and it worked:
<switch>
<Route path={`/anypath/:id`} component={Anycomponent} />
<Route
exact
path="/requestedpath/:id"
render={({ match }) => {
if (!Auth.loggedIn()) {
return <Redirect to={`/signin`} />;
} else {
return <Redirect to={`/anypath/${match.params.id}`} />;
}
}}
/>
</switch>
This functionality has been added to React Router 4 as of 4.3.0. If you're locked into a version before 4.3.x, Gleb's answer is the way to go.
You can do it using generatePath:
import { Switch, Route, Redirect, generatePath } from "react-router";
<Switch>
<Route component={PageOne} path="/one" />
<Route component={PageTwo} path="/two/:id" />
<Route
path="/three/:id"
render={props => (
<Redirect
to={generatePath("/two/:id", {
id: props.match.params.id,
})}
/>
)}
/>
<Route component={NotFoundPage} path="*" />
</Switch>
Short general version using generatePath that worked for me (redirecting from from path to to path, both having the same parameters):
import { Route, Redirect, generatePath } from "react-router"
<Route path={from}
exact
component={({match}) =>
<Redirect to={generatePath(to, match.params)} />} />
In React Router v6+ you could create your own NavigateWithParams like so
import { Navigate, NavigateProps, generatePath, useParams } from 'react-router-dom';
interface Props extends NavigateProps {
to: string;
}
const NavigateWithParams: React.FC<Props> = ({ to, ...props }) => {
const params = useParams();
return <Navigate {...props} to={generatePath(to, params)} />;
};
export default NavigateWithParams;
and use it like so:
<Route path="/old/:param" element={<NavigateWithParams to="/new/:param" />} />

Categories

Resources