I'm coding an authentication with react-router v4 and I'm using the PrivateRoute with render props, like the documentation: Redirects (Auth)
What I'm trying to do is: Whenever the user navigates to a route, I want to dispatch an action to make a request to the backend to verify if he's logged in.
Like this:
// App.js
class App extends Component {
checkAuth = () => {
const { dispatch, } = this.props;
// callback to dispatch
}
render() {
const props = this.props;
return (
<Router>
<div className="App">
<Switch>
<Route exact path="/" component={Login} />
<PrivateRoute
exact
path="/dashboard"
component={Dashboard}
checkIsLoggedIn={this.checkAuth}
/>
{/* ... other private routes here */}
</Switch>
</div>
</Router>
);
}
In PrivateRoute.js I'm listening the route to check if it changes, but when a route changes, this function is called too many times, and that's a problem to dispatch an action to make a request.
// PrivateRoute.js
const PrivateRoute = ({ component: Component, auth, checkIsLoggedIn, ...rest }) => (
<Route
{...rest}
render={props => {
props.history.listen((location, action) => {
if (checkIsLoggedIn) {
// Here I check if the route changed, but it render too many times to make a request
checkIsLoggedIn(); // here is the callback props
}
});
if (auth.login.isLoggedIn) {
return <Component {...props} />;
} else {
return <Redirect to={{ pathname: "/login", state: { from: props.location } }} />
}
}
}
/>
);
I need a help to figure it out a good way to call the backend whenever the route changes.
Creating a Higher Order Component (HOC) is a very clean way to do this. This way, you won't need to create a separate PrivateRoute component, and it would take only one line of change to convert any Component from public to protected, or vice versa.
Something like this should work:
import React from 'react';
import { Redirect } from "react-router-dom";
export function withAuth(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
isUserLoggedIn: false,
isLoading: true
};
}
componentDidMount() {
// Check for authentication when the component is mounted
this.checkAuthentication();
}
checkAuthentication() {
// Put some logic here to check authentication
// You can make a server call if you wish,
// but it will be faster if you read the logged-in state
// from cookies or something.
// Making a server call before every protected component,
// will be very expensive, and will be a poor user experience.
this.setState({
isUserLoggedIn: true, // Set to true or false depending upon the result of your auth check logic
isLoading: false
});
}
render() {
// Optionally, you can add logic here to show a common loading animation,
// or anything really, while the component checks for auth status.
// You can also return null, if you don't want any special handling here.
if (this.state.isLoading) return (<LoadingAnimation />);
// This part will load your component if user is logged in,
// else it will redirect to the login route
if (this.state.isUserLoggedIn) {
return <WrappedComponent authData={this.state} {...this.props} />;
} else {
return <Redirect to={{ pathname: "/login", state: { from: props.location } }} />;
}
}
}
}
Once you have that component in place, all you need to do is use the HOC in any component that you wish to have protected. For example, in your case, the export line in your Dashboard file would be something like this:
/* Dashboard.js */
class Dashboard extends React.Component { ... }
export default withAuth(Dashboard);
and in your App, you can use a simple Route component:
<Route exact path='/dashboard' component={Dashboard} />
Your App does not need to care about which routes are protected, and which ones aren't. In fact, only the actual components need to know that they are protected.
Hope this helps. Cheers! :)
Related
I want to make a component able to redirect when not loggedIn.
Components were made with react, checking auth function works well with redux.
//App.js
class App extends Component {
checkUserInfo () => {
const loggedInfo = storage.get('loggedInfo');
if(!loggedInfo) return;
const { UserActions } = this.props;
UserActions.setLoggedInfo(loggedInfo)
}
constructor(props) {
super(props);
this.checkUserInfo();
}
render() {
console.log(this.props.logged)
return(...)
}
}
export default connect((state) => ({logged: state.user.get('logged')}, (dispatch)=> ...)
and UserActions.setLoggedInfo action is look like this.
...
export default handleActions({
[SET_LOGGED_INFO]: (state, action) => {
return state.set('logged', true)
}
})
...
So, I want situation that component is redirected when auth is not logged in. I made a rendering component <Route/> with condition which is that if state.logged==false, <Redirect to='login/>.
But in very front point, logged is false before executing checkUserInfo function. so when I'm loggedIn, Redirect to /login, and when I'm not loggedIn, Redirect to /login too.
//PrivateRoute.js
...
render() {
const { logged } = this.props;
console.log(logged);
return(
<Route path="/myPage" render={props =>
logged ? <Component/> : <Redirect to={{pathname: '/login'}}/>
}/>
)
}
...
this is screenshot what is logged value in console.
I want to skip very front state before set state by myFunction(checkUserInfo), how can I do.
plz help me.
and sorry to not good english syntax.
You need to check your global state before rendering the private component.
render prop provided by Route is a good place for that
<Route path='/secretarea' render={() =>{
return props.isLoggedIn ? <SecretComp /> : <Login />
}}/>
Set PrivateRoute like this
This could help to check auth in simple way.
What I am trying to do is getting the login user's information from the login component, so that the App component could pass the login user's information to the main component.
Here is some code in the App.js:
state = {
usersArr: [], // all the users
logInUser: 0, // the login user, default value as 0
}
getLogInUser = () => {
return this.state.logInUser;
}
assignLogInUser = user =>{
this.setState({logInUser: user});
console.log("LogIn: " + this.state.logInUser.username);
}
render(){
return(
<div className="App">
<Router>
<div className="App">
<Switch>
<Route path="/main" exact render={() => <Main logInUser={() => this.getLogInUser()}/>}/>
<Route path="" exact render={() => <Landing usersArr={this.state.usersArr} getUser = {this.assignLogInUser}/>}></Route>
</Switch>
</div>
</Router>
)
}
The first page would be landing page and the landing component would be rendered. The landing component will render the login component. The login component will check if the username and password from input would match any user in the usersArr. If it does match, the login component will call the assignLogInUser to assign the login user.
landing.jsx:
render(){
return(
<Login usersArr={this.props.usersArr} getUser={this.props.getUser}/>
)
}
login.jsx:
constructor(props){
super(props);
this.userName = React.createRef(); // username referrence
this.password = React.createRef(); // password referrence
}
validateLogin = () => {
this.props.usersArr.forEach(user => {
if(user.username === this.userName.current.value && user.password === this.password.current.value){
this.props.getUser(user);
}
})
}
The problem is that the logInUser passed into the main is always 0.
Looks like once the route has been set up, it will never update the component it's going to render. No matter what I tried, I would get the default value 0 for logInUser in the main component.
I can probably use redux, but I have to refactor all of the code.
I have tried forceUpdate() in the main component, or using key in the main component. None of them works.
Any help would be appreciated.
I want to check if user is authenticated in my React application. Using this guide.
I wrote a wrapper over my <Route /> class that check, if user is authenticated, then we render component, if not, we just redirect him to sign-in page.
const IsAuthenticatedRoute = function ({ component: Component, ...rest }) {
return (
<Route {...rest} render={async (props) => {
return (
await store.isAuthenticatedAsync() === true // here is the point of troubles
? <Component {...props} />
: <Redirect to={{
pathname: '/sign-in',
state: { from: props.location }
}} />
)
}} />)
}
And I use it in my router like this:
ReactDOM.render(
<Provider store={appStore}>
<Router>
<div>
<Switch>
<Route exact path='/' component={App} />
<IsAuthenticatedRoute path='/protected-route' component={Profile} />
</Switch>
</div>
</Router>
</Provider>
,
document.getElementById('root')
)
I want to execute my async request to the server to check if user is authenticated. I've tried to add async keyword to my functions over await call, but it produces an error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.. I almost tried to use promises, but it isn't help too. When I use Promise inside my function and return <Route /> in .then() operator, React says me: IsAuthenticatedRoute(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
So I expect to handle my async function, and then after I get response from server, give access to my user to visit this page. Is it possible only with sending synchronous request to my server or there're another ways to keep my code async and pass user to the protected page?
An async function cannot be rendered as a component, because you'd be rendering a Promise, not a pure function. Pure functions can be rendered, if they return an instance of a component. Promises must be resolved before they can be rendered.
The solution is to start the asynchronous call when the component is mounted and make the component stateful, so that it can mutate when the call is resolved. You will need to render something while waiting for a response. You can render null, but a loading spinner would be more appropriate. This way we have something to render at all times and won't run into errors trying to render a component that isn't defined yet.
Here's my quick hack at what the component could look like:
class RouteRender extends React.Component {
constructor(props) {
super(props)
this.state = { authorized: null }
}
componentDidMount() {
// setState is called once the asynchronous call is resolved.
store.isAuthenticatedAsync().then(
authorized => this.setState({ authorized})
)
}
render() {
if(this.state.authorized === true) {
const { component: Component, componentProps } = this.props
return <Component {...componentProps} />
} else if(this.state.authorized === false) {
return (<Redirect to={{
pathname: '/sign-in',
state: { from: props.location }
}} />)
}
return <LoadingSpinner />
}
}
const IsAuthenticatedRoute = function ({ component: Component, ...rest }) {
return (
// render is now a function rather than a Promise.
<Route {...rest} render={props => <RouterRender componentProps={props} component={Component} />} />
)
}
I have a very mysterious dilemma: I'm using Reducers to receive a User model from my backend. At first, the reducer returns a state of null (as expected because it's still pending the API call) and then the User model the second time around.
If that User model exists AND the customer tries to navigate to /setup, then we'll let him -- he's "authenticated". However, if he tries to access /setup and the User model doesn't exist, we redirect him back to the root path ("/").
I've made a function that will called within my App.js's Route element that checks this OAuth, called AuthenticatedRoute.js
When we get the initial value from the reducer (of null), we render the AuthenticatedRoute.js, see the value is null and then navigate to the root path. The problem is, when I get the SECOND value from the reducer (of the user model), the AuthenticatedRoute.js doesn't try to re-render at all. Isn't it suppose to? The state of the prop in App.js (the parent component) changed. It's "suppose" to check so we can check if the User model came back and if the customer is allowed to go to the /setup page.
App.js (parent component that contains the Route):
class App extends Component {
state = { isUserSetUp: false }
// If User exists AND its isProfileSetUp is true then change state to TRUE
componentWillReceiveProps(nextProps) {
if (nextProps.auth && nextProps.auth.isProfileSetUp) {
this.setState({ isUserSetUp: false }, function() {
console.log("This is when we receive the User model from the reducer");
});
}
else {
this.setState({ isUserSetUp: false });
}
}
// Upon mount of this component, we receive the User via a reducer endpoint
componentDidMount() {
this.props.fetchUser();
}
render() {
return (
<div className="container">
<BrowserRouter>
<div>
<Header />
<Route exact path="/" component={Landing} />
<Route exact path="/dashboard" component={Dashboard} />
{/* Profile Set up... This is using our AuthenticatedRoute*/}
<Route exact path="/setup" component={AuthenticatedRoute(Setup, this.state.isUserSetUp, Landing)} />
</div>
</BrowserRouter>
</div>
);
}
};
And here is the AuthenticatedRoute:
import React from 'react';
import { withRouter } from 'react-router';
import { Redirect } from "react-router-dom";
export default function requireAuth(Component, isAuthenticated, Landing) {
class AuthenticatedComponent extends React.Component {
componentWillMount() {
this.checkAuth();
}
// We call this only the first time...never the second time around
checkAuth() {
console.log("This is when we run the AuthenticatedRoute");
if (!isAuthenticated) {
// For some reason when this hits, I no longer get the second value (User model) from the reducer. WITHOUT it, I get the second value in this component.
this.props.history.push('/');
}
}
render() {
return isAuthenticated
? <Component { ...this.props } />
: null;
}
}
return withRouter(AuthenticatedComponent);
}
NOTE:
So without the "this.props.history.push('/') within the AuthenticatedRoute, I RECEIVE the second reducer response (I tried console.logging it from the AuthenticatedRoute's render() method)...But the moment I handle the reducer's first response with the this.props.history.push, I never get the second response.
Same deal if I use a within the:
render() {
return isAuthenticated
? <Component { ...this.props } />
: null;
}
Do you guys have any idea? This is too weird.
I'm developing a universal react application using redux. I use react-router v3.
I want to show a progress bar "BEFORE" going to next route (next route is fetching data from API).
for example imagine I am in "Home Page" and I want go to "Submit Page". when I click on the Submit Link (react-router Link) first show a progress bar in "Home Page" and wait for Submit page data fetching and then go to "Submit Page".
My React Routes:
<Route component={App}>
<Route path={HomingRoutes.HomePage} component={HomePage}/>
<Route path={HomingRoutes.SubmitPage} component={SubmitPage}/>
<Route path={HomingRoutes.SearchPage} component={SearchPage}/>
<Route path={`${HomingRoutes.DealsPage}`} component={DealsPage}/>
<Route path={`${HomingRoutes.DealPage}/:id(/:title)`} component={DealPage}/>
<Route path={`${HomingRoutes.Detail}/:id(/:title)`} component={DetailPage}/>
<Route path="*" component={NoMatch}/>
</Route>
in Home Page :
<Link to "/Submit" >Submit</Link>
My Submit page Container code is :
class SubmitContainer extends React.Component {
static readyOnActions(dispatch) {
return Promise.all([
dispatch(SubmitActions.fetchSubmitInitialData()),
]);
}
componentDidMount() {
this.props.fetchSubmitInitialData();
}
}
"fetchSubmitInitialData" is an action creator that fetch data from API.
One solution would be to pass a placeholder component as props to your SubmitPage that will render only when data is fetching.
So you can use something like:
class SubmitContainer extends React.Component {
state = {
loading: true
progress: 0,
}
componentDidMount() {
// fetch some data and update the state
// consider updating the progress more often
this.props.fetchSubmitInitialData()
.then(() => {
this.setState({ loading: false, progress: 100 })
})
}
render() {
const Placeholder = this.props.placeholder
// Show the placeholder when loading
if (this.state.loading) {
return <Placeholder loading progress={this.state.progress} />
}
// Otherwise render your component with the data
return <SubmitPage data={/*..*/}>
}
}
And finally pass you could use the component HomePage as placeholder like this:
<Route path={HomingRoutes.HomePage} component={HomePage}/>
<Route path={HomingRoutes.SubmitPage} render={(props) => (
<SubmitContainer {...props} placeholder={HomePage} />
)}/>
Here I use the render props with React router v4. But I'm sure there is an equivalent for the version 3
Now HomePage will render during data fetching and can use the props loading and progress to show a spinner or something
You can add onEnter hook into your router and add onEnter.js inside your SubmitContainer folder and move fetchSubmitInitialData to the onEnter.js then import your store here and dispatch it. the implementation might looks like this:
Your React-Route
import { onEnterSubmitPage } from './your onEnter path/onEnter'
<Route component={App}>
<Route path={HomingRoutes.HomePage} component={HomePage}/>
<Route path={HomingRoutes.SubmitPage} component={SubmitPage} onEnter={onEnterSubmitPage}/>
<Route path={HomingRoutes.SearchPage} component={SearchPage}/>
<Route path={`${HomingRoutes.DealsPage}`} component={DealsPage}/>
<Route path={`${HomingRoutes.DealPage}/:id(/:title)`} component={DealPage}/>
<Route path={`${HomingRoutes.Detail}/:id(/:title)`} component={DetailPage}/>
<Route path="*" component={NoMatch}/>
</Route>
create onEnter.js file in SubmitPage container:
/**
* Import dependencies and action creators
*/
import { store } from '../../index'
import { fetchSubmitInitialData } from './actions'
/**
* Define onEnter function
*/
export function onEnterSubmitPage() {
store.dispatch(fetchSubmitInitialData())
}
then we can integrate state for progress bar into redux too.
actions.js
/** Import all dependencies here **/
import axios from 'axios'
import { FETCH_SUBMIT_INITIAL_DATA, IS_FETCHING_INITIAL_DATA } from './constants'
export function fetchSubmitInitialData() {
/** this dispatch is from middleware **/
return (dispatch) => {
/** this will set progress bar to true **/
dispatch(fetchSubmitInitialData(true))
/** Your fetching action here, this will depend on your configuration **/
axios.get(`url`, {{ headers: `bearer //your token`}})
.then( (response) => {
dispatch(fetchSubmitInitialData(false))
})
}
}
export function isFetchInitialData(status) {
return {
type: IS_FETCHING_INITIAL_DATA,
status
}
}
so there is no need to fetch the data inside SubmitPage container.
One solution would be to pass a placeholder component as props to your SubmitPage that will render only when data is fetching.
So you can use something like:
class SubmitContainer extends React.Component {
render() {
/** this come from your reducer **/
const { isFetching, submitInitialData } = this.props
// Show the placeholder when loading
if (isFetching) {
return <Loader />
}
// Otherwise render your component
return <SubmitPage data={/*..*/}>
}
}
// Map state to props
const mapStatetoProps = ({ app }) => {
isFetching: //,
submitInitialData: //
}
export default connect(mapStatetoProps, null)(SubmitContainer)