My root App component looks like this:
class App extends Component {
render() {
const state = store.getState();
const { isAuthenticated } = state.auth;
return (
<Provider store={store}>
<BrowserRouter basename="/">
<Switch>
{isAuthenticated ? (
<PrivateRoute
exact
path="/library/:user_id"
component={RessourcesByUser}
/>
) : (
<PublicRoute
exact
path="/library/:user_id"
component={PublicRessourcesByUserPage}
/>
)}
</Switch>
</BrowserRouter>
</Provider>
);
}
}
The problem with isAuthenticated is that when the app is first loaded before the user logs in its value is false.
And it makes sense since the user has not logged in yet.
The problem is that when he logs in, the App component does not mount again (obviously), nor does it get the updated store state in which isAuthenticated is true.
So isAuthenticated here in App component will remain false even though the user is authenticated and its value in the store is true.
It will change to true here once he refreshes because App component will get the updated state then in which isAuthenticated is true.
However, meanwhile this will cause logical bugs such as when he goes to a user's Library right after loading the app and logging-in the first time by clicking on a button that would direct to this path /library/:user_id, he will see the public component PublicRessourcesByUserPage that is meant to be displayed for non-authenticated users instead of RessourcesByUser which is meant for authenticated users.
It's been a while since I've used react-router, but if you want to get the freshest (reactive) state with Redux, then your component will need to be connected to Redux. So something along these lines:
import { connect } from 'react-redux';
class AuthRoutes extends Component {
render() {
return (
<Switch>
{this.props.isAuthenticated ? (
<PrivateRoute
exact
path="/library/:user_id"
component={RessourcesByUser}
/>
) : (
<PublicRoute
exact
path="/library/:user_id"
component={PublicRessourcesByUserPage}
/>
)}
</Switch>
)
}
}
connect((state) => ({ isAuthenticated: state.isAuthenticated }),null)(AuthRoutes);
---
class App extends Component {
render() {
return (
<Provider store={store}>
<BrowserRouter basename="/">
<AuthRoutes />
</BrowserRouter>
</Provider>
);
}
}
Note: I'm making an assumption that Switch will work OK like this.
Related
I'm looking for a efficient way to prevent unauthorized people from accessing specific routing paths.
The cookie that i get from the backend is not readable so i can't do much with it.
I have two endpoints:
/token (POST) to get the cookie with the token
/account (GET) to get the username and role
Short explaination of what i did so far:
Protected all routing by wrapping them with a PrivateRoute component
A redux action is fired after user is attempting to log in. This action calls an api which returns a cookie (jwt) and the data of the user. Username and role will be saved into the 'authStore'. If login succeed the attribute 'isAuthenticated' in the same store is set to True. Only when this boolean is set to true, user can access the wrapped routes.
My problem:
If I close my tab and open a new one, the store resets (which is fine and normal). The cookie is still available tho. So for a good UX I would like to authenticate the user directly. At the moment the user is redirected to the login page. This makes sense because the PrivateRoute component is only accessible if the store attribute isAuthenticated is true. So the user has to login again to update the store.
I tried to dispatch an action in App.js from the method componentDidMount to get the user credentials directly but that didn't help. Since render() is being fired first it won't help.
This is my code:
App.js:
export class App extends PureComponent {
componentDidMount() {
// Action to retrieve user credentials (if available then saves it to the authStore and sets isAuthenticated to true)
this.props.initloginRequest();
}
render() {
return (
<div className="d-flex flex-column h-100 w-100 bg-grey main-container">
<Sprites className="d-none" />
<Router>
<Header />
<Switch>
<Route path="/login" exact component={X1} />
<PrivateRoute path="/" exact component={X2} />
<PrivateRoute path="/details" exact component={X3} />
<PrivateRoute path="/finish" exact component={X4} />
</Switch>
</Router>
</div>
);
}
}
export class PrivateRoute extends Component {
render() {
const { isAuthenticated, component, ...rest } = this.props;
const renderComponent = (props) => {
if (isAuthenticated) {
const ComponentToShow = component;
return <ComponentToShow {...props} />
} else {
return <Redirect to={{ pathname: '/login', state: { from: props.location } }} />
}
};
return (
<Route
{...rest}
render={renderComponent}
/>
)
}
}
export default connect(
(state) => ({
isAuthenticated: state.authStore.isAuthenticated
})
)(PrivateRoute);
There is a possible solution for this by making an api call to /account based on the status code it returns. Whenever the route is changed the call will be made so that is not really what i want. I just want a solution that requires one call at the beginning of the application.
Thanks in advance
I've solved this issue by dispatching an action in index.js (src path):
import './index.css';
import App from './App';
import { initloginRequest } from './actions/auth';
const store = configureStore();
store.dispatch(initloginRequest())
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('root'),
);
This action calls an api and checks if the user is authenticated. So when it reaches my PrivateRoute component, it knows in which direction it should redirect the user.
You should set isFetching in your store/state on true till the action finishes to prevent the App.js from rendering your routes.
export class App extends PureComponent {
render() {
const { isFetching } = this.props
if (isFetching) {
return <div>Loading application...</div>
}
return (
<div className="d-flex flex-column h-100 w-100 bg-grey main-container">
<Sprites className="d-none" />
<Header />
<Switch>
...private route etc...
</Switch>
</div>
);
}
}
export default connect(
(state) => ({
isFetching: state.authStore.isFetching,
}),
(dispatch) => bindActionCreators({
}, dispatch),
)(App);
This should solve the problem.
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
I am having the below Provide which contains the authentication state in it.
export const AuthenticationContext = React.createContext();
export default class AuthenticationProvider extends React.Component {
state = {
isAuthenticated: false
};
render() {
return (
<AuthenticationContext.Provider value={{ state: this.state }}>
{this.props.children}
</AuthenticationContext.Provider>
);
}
}
I have this wrapped to my Routes as below:
<Provider store={store}>
<ConnectedRouter history={history}>
<>
<GlobalStyle />
<AuthenticationProvider>
<SiteHeader />
<ErrorWrapper />
<Switch>
<PrivateHomeRoute exact path="/" component={Home} />
<Route exact path="/login" component={LoginPage}
</Switch>
</AuthenticationProvider>
</>
</ConnectedRouter>
</Provider>
But when I am trying to get context state into the <SiteHeader />, there is nothing passed down by the Context.Provide. My Cosumer is inside <SiteHeader /> is:
class SiteHeader extends React.Component {
render() {
return (
<div>
<AuthenticationContext.Consumer>
{context => (
<header>this is header {context.state.isAuthenticated}</header>
)}
</AuthenticationContext.Consumer>
</div>
);
}
I checked the React devtools, but it's same. Context.Consumer doesn't have value prop from the Provider.
What might be the issue here?
If a value is false, null or undefined, it will not render. I tried your code in a sandbox here. Just added the .toString() in the header, and the value of the boolean is shown in the header.
Context Consumers don't get any data as a props.
instead we pass them a render prop, in this case the children that we pass is the render prop function. and then in the render method of the Consumer something like this happens
render(){
this.props.children(value)
}
this is how we get the value as an argument of the render prop function.
The value of the context provider is not supposed to be passed in through props. You can learn more about render props here
I have an Auth component which is responsible for both login & sign up. I simply receive a prop (isSignup) and display the appropriate form fields. I also use react-router:
<BrowserRouter>
<Route
path="/signup" exact
render={() => <Auth isSignup />} />
<Route
path="/login" exact
render={() => <Auth />} />
</BrowserRouter>
In the Auth component I have a state which holds the values of the form fields.
I'd like to keep the state of Auth after it unmounts, i.e when react-router no longer renders it, so I can keep the values when the user switches between /signup and /login.
Is there a way to do this without global state management (e.g Redux)?
If you don't want to use Redux for this purpose, but you still want to share this specific state across your app, then there are a few ways to go about it. If you want to stay "Reacty", then you really have 2 options:
Pass state as props
Use a Provider
If you just want something that works but isn't necessarily the React way, you have more options (like setting window.isLoggedIn and toggling it at will... which I don't recommend).
For this answer, we'll focus on the "Reacty" options.
Pass state as props
This is the default approach with React. Track your isLoggedIn variable somewhere at the root of your component tree, then pass it down to your other components via props.
class App extends Component {
state = { isLoggedIn: false }
logIn = () => this.setState({ isLoggedIn: true })
logOut = () => this.setState({ isLoggedIn: false })
render() {
return (
<BrowserRouter>
<Route
path="/signup"
exact
render={() => (
<Auth
isSignup
isLoggedIn={this.state.isLoggedIn}
logIn={this.logIn}
logOut{this.logOut}
/>
)}
/>
<Route
path="/login"
exact
render={() => (
<Auth
isLoggedIn={this.state.isLoggedIn}
logIn={this.logIn}
logOut{this.logOut}
/>
)}
/>
</BrowserRouter>
)
}
}
This works but gets tedious as you need to pass isLoggedIn, logIn, and logOut to almost every component.
Use a Provider
A Provider is a React component that passes data to all its descendants via context.
class AuthProvider extends Component {
state = { isLoggedIn: false }
logIn = () => this.setState({ isLoggedIn: true })
logOut = () => this.setState({ isLoggedIn: false })
getChildContext() {
return {
isLoggedIn: this.state.isLoggedIn,
logIn: this.logIn,
logOut: this.logOut
}
}
static childContextTypes = {
isLoggedIn: PropTypes.bool,
logIn: PropTypes.func,
logOut: PropTypes.func
}
render() {
return (
<>
{this.props.children}
</>
)
}
}
Then, you initialize your app by wrapping the whole thing in AuthProvider:
<AuthProvider>
<BrowserRouter>
// routes...
</BrowserRouter>
</AuthProvider>
And in any component of your app, you will be able to access isLoggedIn, logIn, and logOut via this.context.
You can read more about the Provider pattern and how it works in this excellent article from Robin Wieruch.
notes:
It's common for components to receive context as props using a higher order component, but I left that out of the answer for brevity. Robin's article goes into this further.
You may recognize the Provider pattern as one that react-redux and other state management libraries utilize. It's fairly ubiquitous at this point.
You could create a wrapper which stores the state above the components:
class AuthWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return ([
<Route
key="signup"
path="/signup" exact
>
<Auth
state={this.state}
setState={(state) => this.setState(state)}
isSignup
/>
</Route>,
<Route
key="login"
path="/login" exact
>
<Auth
state={this.state}
setState={(state) => this.setState(state)}
/>
</Route>
]);
}
}
Then this component can be used, which stores the state over those routes.
<BrowserRouter>
<AuthWrapper />
</BrowserRouter>
You would just need to use the state and setState in the params instead in the Auth component.
I'm building an article search with React [15.6.1] and Router [4.1.1] and noticed that when trying to access an article directly the previous component is loaded, even thou it's not the one that's being called
// === Layout ==================
class Layout extends React.Component {
render() {
return (
<Provider store={this.props.store}>
<HashRouter>
<div>
<Switch>
<Route exact path="/" component={SearchFilter} />
<Route path="/article/:guid" component={Article} />
</Switch>
</div>
</HashRouter>
</Provider>
);
}
}
// === SearchFilter ==================
class SearchFilter extends React.Component {
componentDidMount() {
console.log('SearchFilter Did Mount');
}
render() {
return (
<div>...</div>
);
}
}
// === Article ==================
class Article extends React.Component {
componentDidMount() {
console.log('Article Did Mount');
}
render() {
return (
<div>...</div>
);
}
}
So when going to the root localhost:3000/#/ it prints
// SearchFilter Did Mount
And when I access an article directly like this localhost:3000/#/article/123456 it prints
// SearchFilter Did Mount
// Article Did Mount
So my question is, how can I prevent it from running the previous route?
Because I would like to dispatch some actions there that would trigger some ajax calls to the webservice.
Thanks
Try this instead :
<HashRouter basename="/">
<div>
<Switch>
<Route exact path="/search" component={SearchFilter} />
<Route path="/article/:guid" component={Article} />
</Switch>
</div>
</HashRouter>
Edit:
For me its working well... So like said... there is something really weird and hidden happening on your machine, or you just put / and then rewrite url to /#/article/123 and it cause the first log stays in the console, but its from the previsous url "/" and if you reload the browser by F5 on the new url /#/article/123 you will see only the "Article did mount"