React-router how to load component based on props? - reactjs

First my routes:
const routes = (
<Route path="/" component={authWrapper(App)}>
<IndexRoute getComponent={resolveIndexComponent} />
<Route path="about" getComponent={resolveAboutComponent} />
</Route>
);
Notice how the root route gets wrapped with "authWrapper", there I check if user has JWT and dispatch to redux store the result.
The question/problem: How do I load different "child" component for authenticated and non-authenticated users when they enter IndexRoute ?
The above resolveIndexComponent just loads '/domains/home' which contains:
const mapStateToProps = (state) => ({ authenticated: state.common.currentUser });
const App = (props) => {
if (props.authenticated) {
return (<Authenticated />);
}
return (<Unauthenticated />);
};
export default connect(mapStateToProps)(App);
Could I skip this and load either <Authenticated /> or <Unauthenticated /> directly from react-router ? (based on either props or even what authWrapper returns)

Related

How to use Protected Routes with react-router-dom V6 and typescript?

I am trying to implement a protected route to only allow logged in users to access my app. The code I have written seems to work, I am redirected to my login page when I try to access the homepage without being logged in, however once I am logged in I can access the page but I does not render and I get the following error: Click here for error
I have tried multiple methods and wrapping the element in my protected route seems like the V6 way of doing things, however it doesn't seem to work for me:
My protected route
interface PrivateRouteProps extends RouteProps {
}
const PrivateRoute: React.FC<PrivateRouteProps> = ({...rest}) => {
const auth = useAuth();
if (auth?.user === null) {
return <Navigate to='/'/>;
}
return <Route {...rest}/>;
};
export default PrivateRoute;
My app (route usage)
function App() {
useEffect(() => {
API
.get('api', '/reservation', {})
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error.response);
});
}, [])
return (
<Router>
<Routes>
<Route path='/' element={<LoginPage />}/>
<Route path='/consultAndReserve' element={<PrivateRoute><Navbar/><ConsultReserve/></PrivateRoute>} />
<Route path='/consultAndReserve/:date' element={<><Navbar/><ConsultReserveWithDate/></>}/>
<Route path='/myReservations' element={<><Navbar/><Reservations/></>}/>
<Route path='/tracing' element={<><Navbar/><Tracing/></>}/>
</Routes>
</Router>
);
}
What am I doing wrong?
It's fairly trivial to create a "PrivateRoute" component, even in TypeScript.
In your case you can't directly render a Route component as the error points out. This was a breaking change between RRDv5 and RRDv6. You can render the children prop since you are directly wrapping the components you want to protect.
Example:
const PrivateWrapper = ({ children }: { children: JSX.Element }) => {
const auth = useAuth();
return auth?.user ? children : <Navigate to="/" replace />;
};
Usage:
<Routes>
...
<Route
path="/consoleAndReserve"
element={(
<PrivateWrapper>
<Navbar />
<ConsultReserve />
</PrivateWrapper>
)}
/>
...
</Routes>

How to make header component know if user is logged in in React?

I have a React project that has a HeaderComponent that exists for all routes in project like this:
function App() {
return (
<Fragment>
<Router>
<HeaderComponent />
<Routes>
<Route path="/login" element={<Login />}></Route>
<Route path="/register" element={<Register />}></Route>
<Route path="/" element={<LandingPage />}></Route>
</Routes>
<FooterComponent />
</Router>
</Fragment>
);
}
And my problem is that the <HeaderComponent> is rendered when the website first loads but when the user logs in, the <HeaderComponent> is not aware of the changes because the component has already mounted.
So in my <HeaderComponent>, the componentDidMount function looks like this:
componentDidMount() {
AuthService.authorizeUser()
.then((r) => {
this.setState({ loggedIn: true });
})
.catch((error) => {
this.setState({ loggedIn: false });
});
}
This only works if I refresh the page.
Basically, if a user successfully logs in (from the <Login> component), what is the proper way of making my HeaderComponent aware of this?
You can use Context API to make AuthContext to share global state within your app:
// AuthContext.js
export const AuthContext = React.createContext({});
export const AuthProvider = ({
children,
}) => {
// your context logic
return (
<AuthContext.Provider value={yourAuthValue}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => React.useContext(AuthContext);
// Layout.js
import { Outlet } from 'react-router-dom'
// Using `Outlet` to render the view within layout
export const Layout = () => {
return (
<>
<HeaderComponent />
<Outlet />
<FooterComponent />
</>
)
}
// HeaderComponent.js
import { useAuth } from './AuthContext'
export const HeaderComponent = () => {
// get state from auth context
const { isLoggedIn } = useAuth()
return // rest of your code
}
// App.js
function App() {
return (
<Fragment>
<-- Wrap your app with AuthContext let other components within your app can access auth state !-->
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<LandingPage />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Route>
</Routes>
</BrowserRouter>
</AuthProvider>
</Fragment>
);
}
There are a couple of ways to do so.
When you're facing a situation where you need to share the same state between multiple components, lifting the state up should be the first thing to try Check this codesandbox.
And some great blogposts to read, KCD - Prop Drilling, KCD - State Management with React
Such approach may cause "prop drilling" when you need the same state in deeply nested components and that's where the context API comes in handy.
codesandbox

React Switch always going to first Route

Here if I am hitting "/" or "/reset_password" or any other route always first component in PurelyUnauthenticatedRoutes gets called i.e. PublicRoute with message ROUTE_CONSTANTS.RESET_PASSWORD
<Router>
<Switch>
<PurelyUnauthenticatedRoutes />
<PublicRoute key={1} redirect={false} path="/" component={CheckAuthenticatedRoutes} />
</Switch>
</Router>
const PurelyUnauthenticatedRoutes = (props) => {
return [
<PublicRoute
{...props}
key={"/reset_password"}
redirect={false}
path={"/reset_password"}
render={(routeProps) => <div>{ROUTE_CONSTANTS.RESET_PASSWORD}</div>}
/>,
];
};
const PublicRoute = (props) => {
const { userAuthenticationDetails, redirect } = props;
const isLoggedIn = (userAuthenticationDetails && userAuthenticationDetails.isLoggedIn) || false;
if (isLoggedIn && redirect) {
return <Redirect to={"/dashboard"} />;
}
return <Route {...props} />;
};
export default PublicRoute;
It's the return statement on your PurelyUnauthenticatedRoutes component that's doing this. You are returning an array of PublicRoute components. React does not know how to render this properly. Instead, you should return multiple routes as children of a Switch component.
It's fine to have multiple Switch statements in a Router. All Route components need to be direct children of a Switch (unless you want to render multiple Routes).
You do need to figure some things out regarding your match conditions because the Switch at the top-level of the the Router means that traffic will go to either PurelyUnauthenticatedRoutes OR PublicRoute - not both. Currently all traffic gets handled by PurelyUnauthenticatedRoutes so that PublicRoute component with key={1} is never shown.
const PurelyUnauthenticatedRoutes = (props) => {
return (
<Switch>
<PublicRoute
{...props}
key={"/reset_password"}
redirect={false}
path={"/reset_password"}
render={(routeProps) => <div>{ROUTE_CONSTANTS.RESET_PASSWORD}</div>}
/>
</Switch>
);
};
Note: you aren't currently calling PurelyUnauthenticatedRoutes with any props when you use it in the App so you don't actually need to accept or pass down that props object if you don't want to.

React router dom redirect problem. Changes url, does not render component

Problem: When I use history.push(), I can see that browser changes url, but it does not render my component listening on the path. It only renders if I refresh a page.
App.js file:
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import { Provider } from "react-redux";
import PropTypes from "prop-types";
//Components
import LoginForm from "../LoginForm/LoginForm";
import PrivateRoute from "../PrivateRoute/PrivateRoute";
import ServerList from "../ServerList/ServerList";
const App = ({ store }) => {
const isLoggedIn = localStorage.getItem("userToken");
return (
<Router>
<Provider store={store}>
<div className="App">
{isLoggedIn !== true && (
<Route exact path="/login" component={LoginForm} />
)}
<PrivateRoute
isLoggedIn={!!isLoggedIn}
path="/"
component={ServerList}
/>
</div>
</Provider>
</Router>
);
};
App.propTypes = {
store: PropTypes.object.isRequired
};
export default App;
Inside my LoginForm, I am making a request to an API, and after doing my procedures, I use .then() to redirect my user:
.then(() => {
props.history.push("/");
})
What happens: Browser changes url from /login to /, but component listening on / route is not rendered, unless I reload page.
Inside my / component, I use useEffect() hook to make another request to API, which fetches data and prints it inside return(). If I console.log inside useEffect() it happens twice, I assume initial one, and when I store data from an API inside component's state using useState() hook.
EDIT: adding PrivateRoute component as requested:
import React from "react";
import { Route, Redirect } from "react-router-dom";
const PrivateRoute = ({ component: Component, isLoggedIn, ...rest }) => {
return (
<Route
{...rest}
render={props =>
isLoggedIn === true ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: "/login" }} />
)
}
/>
);
};
export default PrivateRoute;
What I tried already:
1) Wrapping my default export with withRouter():
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginForm));
2) Creating custom history and passing it as prop to Router.
react-router-dom version is ^5.0.1. react-router is the same, 5.0.1
You have at two mistakes in your code.
You are not using <switch> component to wrap routes. So all routes are processed at every render and all components from each <route> are rendered.
You are using local store to exchange information between components. But change in local store is invisible to react, so it does not fire component re-rendering. To correct this you should use local state in App component (by converting it to class or using hooks).
So corrected code will look like
const App = ({ store }) => {
const [userToken, setUserToken] = useState(localStorage.getItem("userToken")); // You can read user token from local store. So on after token is received, user is not asked for login
return (
<Router>
<Provider store={store}>
<div className="App">
<Switch>
{!!userToken !== true && (
<Route exact path="/login"
render={props => <LoginForm {...props} setUserToken={setUserToken} />}
/>
)}
<PrivateRoute
isLoggedIn={!!userToken}
path="/"
component={ServerList}
/>
</Switch>
</div>
</Provider>
</Router>
);
};
And LoginForm should use setUserToken to change user token in App component. It also may store user token in local store so on page refresh user is not asked for login, but stored token is used.
Also be sure not to put anything between <Switch> and </Switch> except <Route>. Otherwise routing will not work.
Here is working sample

React router receives match params but does not update the component

I'm using react-boilerplate for my project. When changing the router param, the component does not rerender. The router looks like this.
<switch>
<route exact path="/" render={()><homepage {...this.props}/> } />
<route exact path="/practice-areas" component={PracticeAreasLandingPage}/>
<route path="/practice-areas/:practiceAreasItem" component={PracticeAreas}/>} />
<route component={ notfoundpage } />
</switch>
The component looks like this.
class PracticeArea extends PureComponent {
constructor(props) {
super(props);
}
componentWillMount() {
this.props.onInit();
}
render() {
const pa = this.props.practiceArea;
const randPas = this.props.randPracticeAreas;
....
export function mapDispatchToProps(dispatch) {
return {
onInit: () => {
dispatch(loadPracticeArea());
dispatch(loadRandomPracticeAreas());
}
};
}
const mapStateToProps = createStructuredSelector({
practiceArea: makeSelectPracticeArea(),
loading: makeSelectLoading(),
error: makeSelectError(),
randPracticeAreas: makeSelectRandomPracticeAreas(),
randLoading: makeSelectRandomLoading(),
randError: makeSelectRandomError(),
});
const withConnect = connect(
mapStateToProps,
mapDispatchToProps,
);
const withReducer = injectReducer({
key: 'practiceArea',
reducer
});
const withSaga = injectSaga({
key: 'practiceArea',
saga
});
export default compose(
withReducer,
withSaga,
withConnect,
)(PracticeArea);
When clicking on a Link that has a route like '/practice-areas/some-route', the component does not update.
Try moving the PracticeAreas Route before the PracticeAreasLandingPage Route
<Switch>
<Route exact path="/" render={()><homepage {...this.props}/> } />
<Route path="/practice-areas/:practiceAreasItem" component={PracticeArea} />
<Route path="/practice-areas" component={PracticeAreasLandingPage}/>
<Route component={ NotFoundPage } />
</Switch>
Order of Routes is important in a Switch component, since it goes down the list to find a path that is satisfied by the url.
If you navigate to localhost:3000/practice-areas/123 and the PracticeAreasLandingPage Route comes before PracticeArea, you still satisfy the necessary condition for PracticeAreasLandingPage which is just "practice-areas" so Switch ends up rendering that first. And since you are already on that route to begin with, it gives the appearance that nothing was updated.
Swapping the position of the two Routes would resolve this because now you're telling Switch to go down the list and check whether the url, localhost:3000/practice-areas/123 satisfies the path for PracticeArea "/practice-areas/:practiceAreasItem" first.

Resources