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.
Related
I have this privateRoute component that I use to manage authentication:
const PrivateRoute = ({ component: Component, auth, ...rest }) => {
// This gets logged twice when I navigate in the app
// Using history.push("/url")
// And gets logged once when I type the url and hit enter on the browser
console.log(
"===============INSIDE PrivateRoute Component========================"
);
return (
<Route
{...rest}
render={(props) =>
auth.isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect to="/login" />
)
}
/>
);
};
The strange thing is that this component gets logged twice when I navigate in the app. For example, when I hit a button that triggers this code:
this.props.history.push("/edit-page-after-login");
And I have this in App.js:
<PrivateRoute
path="/edit-page-after-login"
component={EditProfileAfterLogin}
/>
And I have a component that gets rendered in that route:
export default class EditProfileInSettings extends Component {
componentDidMount() {
console.log(
"🚀 ~ file: EditProfileInSettings.js ~ line 5 ~ EditProfileInSettings ~ componentDidMount ~ componentDidMount"
);
}
render() {
return <div>Test</div>;
}
}
So when I navigate to that component using history.push, this gets logged:
===============INSIDE PrivateRoute Component========================
🚀 ~ file: EditProfileInSettings.js ~ line 5 ~ EditProfileInSettings ~
componentDidMount ~ componentDidMount
===============INSIDE PrivateRoute Component========================
For some strange reason, PrivateRoute component gets called TWICE which is causing me some issues in the logic I am trying to implement.
But, when I write the url in the browser and enter, it behaves correctly and it only gets called ONCE:
===============INSIDE PrivateRoute Component========================
🚀 ~ file: EditProfileInSettings.js ~ line 5 ~ EditProfileInSettings ~
componentDidMount ~ componentDidMount
Any idea what's going on here?
EDIT 1: I have noticed this error only occurs when I do an API call to the backend inside the component:
class PrivateRouteTestComponent extends Component {
componentDidMount() {
console.log("PrivateRouteTestComponent.componentDidMount is called!");
// If I comment this out, the problem will not occur.
// It only occurs with this line
// It does an API call to the backend to get user profile
this.props.getAuthenticatedUserProfile();
}
render() {
return (
<div>
<button
onClick={() => {
this.props.history.push("/videos-page");
}}
>
Home
</button>
<h6>Private route test component</h6>
</div>
);
}
}
EDIT 2: I finally found why this error occurs. Calling a function that dispatches something to the store will update the PrivateRoute so it will get called again:
class PrivateRouteTestComponent extends Component {
componentDidMount() {
console.log("PrivateRouteTestComponent.componentDidMount is called!");
// This doesn't cause the problem
testBackendCall();
// This causes the problem
// Because it dispatches an action to the store
// So PrivateRoute gets updated
this.props.testBackendCallThatDispatchesSomethingToTheStore();
}
render() {
return (
<div>
<button
onClick={() => {
this.props.history.push("/videos-page");
}}
>
Home
</button>
<h6>Private route test component</h6>
</div>
);
}
}
You're mixing functional components and React class-based components, and expect that the logging in PrivateRoute and the one in EditProfileInSettings to be done at the same moment in the rendering cycle - but it's not.
In EditProfileInSettings, you log on the mounting phase (componentDidMount), which happens once in a component's rendering (if not unmounted).
In PrivateRoute, you log on the rendering phase (think the equivalent of render on a class Component), which happens every time React needs to update your component because of its props being changed.
If you want the two logging to be equivalent, either place your logging in a useEffect() on your PrivateRoute, or place your logging at the render() on your EditProfileInSettings.
Then, to know why your functional component is rendered two times, log all your props and spot the differences between two cycles.
This is what solved my problem. Using React hooks to execute code in functional component only when it's mounted.
const PrivateRoute = ({ component: Component, auth, ...rest }) => {
React.useEffect(() => {
// EXECUTE THE CODE ONLY ONCE WHEN COMPONENT IS MOUNTED
}, []);
return (
<Route
{...rest}
render={(props) =>
auth.isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect to="/login" />
)
}
/>
);
};
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.
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! :)
I have a "home" component with links, and when you click a link the product component is loaded with the product. I also have another component which is always visible, showing links to the "recently visited products".
These links don't work when on a product page. The url updates when I click the link, and a render occurs, but the product component doesn't update with the new product.
See this example:
Codesandbox example
Here are the routes in index.js:
<BrowserRouter>
<div>
<Route
exact
path="/"
render={props => <Home products={this.state.products} />}
/>
<Route path="/products/:product" render={props => <Product {...props} />} />
<Route path="/" render={() => <ProductHistory />} />
<Link to="/">to Home</Link>
</div>
</BrowserRouter>;
The links in ProductHistory look like this:
<Link to={`/products/${product.product_id}`}> {product.name}</Link>
So they match the Route path="/products/:product".
When I am on a product page and try to follow a ProductHistory link, the URL updates and a render occurs, but the component data doesn't change. In the Codesandbox example you can uncomment the alert in Product components render function to see that it renders when you follow the link, but nothing happens.
I don't know what the problem is...Can you explain the problem and find a solution? That would be great!
Along with componentDidMount, You also need to implement the componentWillReceiveProps or use getDerivedStateFromProps(from v16.3.0 onwards) in Products page since the same component is re-rendered with updated params and not re-mounted when you change the route params, this is because params are passed as props to the component and on props change, React components re-render and not re-mounted.
EDIT: from v16.3.0 use getDerivedStateFromProps to set/update state based on props(no need to specify it in two different lifecyle methods)
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.match.params.product !== prevState.currentProductId){
const currentProductId = nextProps.match.params.product
const result = productlist.products.filter(obj => {
return obj.id === currentProductId;
})
return {
product: result[0],
currentId: currentProductId,
result
}
}
return null;
}
Prior v16.3.0, you would use componentWillReceiveProps
componentWillReceiveProps(nextProps) {
if (nextProps.match.params.product !== this.props.match.params.product) {
const currentProductId = nextProps.match.params.product
const result = productlist.products.filter(obj => {
return obj.id === currentProductId;
})
this.setState({
product: result[0],
currentId: currentProductId,
result
})
}
}
Working codesandbox
As Product component is already loaded it will not reload. You have to handle new product id in the below method of component
componentWillReceiveProps(nextProps) {
if(nextProps.match.params.name.product == oldProductId){
return;
}else {
//fetchnewProduct and set state to reload
}
With latest version of react(16.3.0 onwards)
static getDerivedStateFromProps(nextProps, prevState){
if(nextProps.productID !== prevState.productID){
return { productID: nextProps.productID};
}
else {
return null;
}
}
componentDidUpdate(prevProps, prevState) {
if(prevProps.productID !== this.state.productID){
//fetchnewProduct and set state to reload
}
}
Although all the above-mentioned ways will work, I don't see a point to use getDerivedStateFromProps.
Based on React docs, "if you want to re-compute some data only when a prop changes, use a memoization helper instead".
Here, instead, I would suggest simply using componentDidUpdate along with changing the Component to PureComponenet.
With reference to React docs, PureComponenets only rerender if at least one state or prop value changes. Change is determined by doing a shallow comparison of state and prop keys.
componentDidUpdate = (prevProps) => {
if(this.props.match.params.id !== prevProps.match.params.id ) {
// fetch the new product based and set it to the state of the component
};
};
Please note that the above only work if you change the Component to PureComponent, and obviously, you need to import it from React.
If you aren't maintaining state in your component, you can use componentDidUpdate without the need for getDerivedStateFromProps:
componentDidUpdate(prevProps) {
const { match: { params: { value } } } = this.props
if (prevProps.match.params.value !== value){
doSomething(this.props.match.params.value)
}
}
I have a PageBuilder component that dynamically builds edit/list pages according to a configuration file. I want to have dynamic routes (like "/collection/list", "/collection/edit/123123", "/dashboard", etc.) that use the same PageBuilder component.
I'm having trouble getting this to work - if I'm in "/collection/list" for example, when clicking on a link to "/collection/edit/1231" doesn't work. Only a refresh to that URL works (and vice-versa).
I tried putting my initialization code PageBuilder's componentWilLReceiveProps but it seems to call it every second.
My routes look like this:
<Route path="/" component={App}>
<IndexRedirect to="/dashboard" />
<Route path="/:page/:collection/:action(/:entity_id)" component={PageBuilder} />
<Route path="/:page" component={PageBuilder} />
</Route>
And my PageBuilder:
constructor(props) {
super(props);
this.createSectionsHTML = this.createSectionsHTML.bind(this);
this.onChange = this.onChange.bind(this);
this.onSave = this.onSave.bind(this);
}
getPageName() {
return this.props.params.page.replace(/-/g, '_').toLowerCase();
}
componentWillReceiveProps(props) {
this.action = this.props.params.action;
}
componentWillMount() {
let pageName = this.getPageName();
this.props.dispatch(setInitialItem(pageName));
}
componentDidMount() {
let pageName = this.getPageName();
let { collection, entity_id } = this.props.params;
if (collection && entity_id) {
let { dispatch } = this.props;
dispatch(getCollectionEntity(collection, entity_id, pageName));
}
}
Any ideas of how to re-render the page each time I redirect to a different route?
It would be great if I could unmount and re-mount the component when redirecting, but I'm not sure how to go about telling React Router to do that....
Thanks!
Make this.state such that it will control how your component gets rendered.
Now, within componentWillReceiveProps, check the nextProps argument
componentWillReceiveProps(nextProps, nextState) {
if( <check in nextProps if my route has changed> ) {
let newState = Object.assign({}, this.state);
// make necessary changes to the nextState Object by calling
// functions which would change the rendering of the current page
this.setState({ nextState });
}
}
This would make componentWillReceiveProps take action only when the route changes.
Now in your render function,
render() {
const { necessary, variables, to, render } = this.state;
let renderVariables = this.utilityFunctionsReqToRender(someArgs);
return (
<toRenderJsx>
...
</toRenderJsx>
)
}
This would make your component "refresh" whenever the route changes.
componentWillReceiveProps is deprecated since React 16.3.0
(as says a warning in the browser console)
Reference :
https://reactjs.org/docs/react-component.html#componentdidupdate
So componentDidUpdate can be used to get the new state and reload data depending on params
componentDidUpdate(prevProps, prevState, snapshot) {
console.log("componentDidUpdate " +prevState.id);
reloadData(prevState.id);
}