This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 2 years ago.
In my project after login, the Navbar has to change.
class App extends React.Component {
render(){
return(
<BrowserRouter>
<div className="App">
{firebase.auth().onAuthStateChanged(user =>{
if(user){
return(
<UserNavBar/>
)
}else{
return(
<CommonNavBar/>
)
}
})}
<Route path="/Home" component={Home}/>
<Route path="/Photos" component={Photos}/>
<Route path="/RegisterPage" component={RegisterPage}/>
<Route path="/LoginPage" component={LoginPage}/>
<Route path="/Account" component={Account}/>
</div>
</BrowserRouter>
)
}
}
export default App
But nothing is returned, I tried to debug it many times:
The system detects that the user is login
If I console log after the if it's work
Make sure whenever you are doing Async operations in a component which is in your case a class component, do the same in componetDidMount(). then change the state of your component by using setState.
you can see the lifecycle of the react here: https://reactjs.org/docs/state-and-lifecycle.html
for eg:
class App extends React.Component {
constructor(props){
super(props);
this.state = {
user: null
}
}
componentDidMount() {
firebase.auth().onAuthStateChanged(user => user && this.setState({user}) )
}
render(){
const { user } = this.state;
return(
<BrowserRouter>
<div className="App">
{user ? <UserNavBar/> : <CommonNavBar/>}
<Route path="/Home" component={Home}/>
<Route path="/Photos" component={Photos}/>
<Route path="/RegisterPage" component={RegisterPage}/>
<Route path="/LoginPage" component={LoginPage}/>
<Route path="/Account" component={Account}/>
</div>
</BrowserRouter>
)}
}
export default App
or you can use hooks instead of class component.
I believe the problem is that you misunderstand the Async code (Promises) and template rendering concept. You can't just return some component out of Promise and expect it to appear in the result. Here is a bit different approach that you can use for start:
import React, { useState, useEffect } from 'react';
const NavBar = ({ user, isLoading }) => {
const [isLoading, setIsLoading] = useState(true);
const [user, setUser] = useState(null);
useEffect(() => {
firebase.auth().onAuthStateChanged(user =>{
setUser(user);
setIsLoading(false);
});
}, []);
if (isLoading) {
return <div>Some navigation placeholder without content while info is loading....</div>
}
return user ? <UserNavBar/> : <CommonNavBar/>;
}
const App = () => {
return(
<div className="App">
<NavBar />
<BrowserRouter>
<Route path="/Home" component={Home}/>
<Route path="/Photos" component={Photos}/>
<Route path="/RegisterPage" component={RegisterPage}/>
<Route path="/LoginPage" component={LoginPage}/>
<Route path="/Account" component={Account}/>
</BrowserRouter>
</div>
)
};
export default App
Important things here:
In onAuthStateChanged we don't return some component, but we set the state data with user information. Setting state would re-render component and depends on current existing user data on hands, component would render the result.
While data is loading, it's a good practice to show some placeholder (empty nav) to avoid too much glitching
We've extracted Navigation data and rendering logic to separate component - to not pollute common App component with very specific code.
Code looks a bit differently because of Hooks API which are more "fancy and popular" these days in React community.
In case of questions, feel free to add comments, I'd be glad to help.
Related
I am trying to implement a feature when the user logs into the app and they have not completed their profile, they should be redirected to a certain URL, So everywhere they try to go (except logout) they should be redirected to the complete-profile URL.
I'm handling routing with react-router-dom package.
App.js
class App extends Component {
async componentDidMount() {
const logged = isLoggedIn();
if (logged) {
const completed = await hasCompleted();
if (!completed) this.props.history.replace("/complete-profile"); //error showing here
}
}
render() {
return (
<React.Fragment>
<NavBar />
<main className="container">
<Switch>
<Route path="/complete-profile" component={CompleteProfile} />
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
<Route path="/logout" component={Logout} />
<Route path="/" exact component={Home} />
</Switch>
</main>
</React.Fragment>
);
}
}
So basically what I'm trying to do here in componentDidMount method is: I first check if the user is logged in. Then check if the user has completed their profile, and if it's not completed then it should redirect to /complete-profile URL.
But with this method I'm facing an error with this.props.history.replace because it does not take any props when this method is getting called I guess, and the error that is showing is this:
Unhandled Rejection (TypeError): Cannot read property 'replace' of undefined
Which is the proper way to implement this one?
Because I dont think that I should implement these 4 lines of code checking for completed profile in every single component.
In the App component the history prop is not available (undefined) because the following props:
history
location
match
are passed from Route component to its children (CompleteProfile, Home,....). So you can not use them in the App component.
Instead you can create your own Route component:
class CompleteProfile extends Component {
state = {
completed: false
};
async componentDidMount() {
const logged = isLoggedIn();
if (logged) {
const completed = await hasCompleted();
this.setState({completed});
//error showing here
}
}
render() {
const { component: Component, ...rest } = this.props;
const { completed } = this.state;
return (
<Route {...rest} render={(props) => (
completed
? <Component {...props} />
: <Redirect to='/complete-profile' />
)} />
)
}
}
and use it instead of Route like this:
<CompleteProfile path='/' exact component={Home} />
This is the general idea you can refactor the code as you want.
Have a look at the description of the Route component. You'll see that three props are injected for each component that's rendered using <Route /> in your code for example. <Route path="/login" component={Login} /> These props are match location history
In your code App is not rendered using Route. Because of this, you don't have access to these three injected props. That's the reason why you get the error that history is undefined.
To redirect the user, use something like the below instead, where you conditionally render a redirect or the UI depending if data is present.
class App extends Component {
constructor(props){
super(props);
this.state = {
fetchingStatus: true,
completed: false
};
}
async componentDidMount() {
const logged = isLoggedIn();
if (logged) {
const completed = await hasCompleted();
if (!completed) this.setState({ fetchingStatus: false, completed: true })
}
}
render() {
const { fetchingStatus, completed } = this.state;
// Render nothing, or a spinner as loading indicator
if (fetchingStatus) return null; // waiting for details...
// if data was fetched and registeration not completed, render redirect.
if (!fetchingStatus && !completed) return <Redirect to="/complete-profile" />
// Render switch and nav.
return (
<React.Fragment>
<NavBar />
<main className="container">
<Switch>
<Route path="/complete-profile" component={CompleteProfile} />
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
<Route path="/logout" component={Logout} />
<Route path="/" exact component={Home} />
</Switch>
</main>
</React.Fragment>
);
}
}
More on React Router's redirect component here
This sort of TypeError becomes intimately familiar when working with React. It occurs when you attempt to use methods from an object that is "undefined".
You can ensure that this.props.history is not undefined like this:
if (this.props.history && !completed)
this.props.history.replace("/complete-profile"); //error showing here
The npm package 'Lodash' may also be of use in avoiding these kinds of complications.
This just a cheap trick. If you want to go to any url (including react or any other front end framework), just do :
window.location.replace(str)
str can be absolute path like "https://abcd.com", or relative-path like ("/complete-profile" which will become baseurl + "/complete-profile")
if (logged) {
const completed = await hasCompleted();
if (!completed) window.location.replace("/complete-profile"); //error showing here
}
Also #zacks method also works
Here is my navigation component:
import React from 'react'
class Navigation extends React.Component {
constructor(props) {
super(props);
this.state = {
type: 'signUp', // or login
showModal: false,
isLoggedIn: false,
}
}
...some code
render() {
const { showModal, type, isLoggedIn } = this.state
console.log(this.props.location); // all problem is this, I'm not getting it in console
return(
...some more code
)
}
}
export default withRouter(Navigation)
And here is where it it been used in app.js
class App extends React.Component {
render () {
return(
<Router>
<Fragment>
<Navigation /> // <= right there
<Switch>
<Route exact path='/' component={HomePage}/>
<Route exact path='/search' component={HomePage}/>
<Route component={Lost} />
</Switch>
</Fragment>
</Router>
)
}
}
I want to get updated route props like match and location and history in my <Navigation /> component but I get it only when the first time that component mounts on the DOM, in my other components I update the route using window.history.pushState but I am not able to get route props from withRouter after link in the browser is been updated.
I update route with window.history.pushState because:
I could not find any way to update just link in the address bar without showing user or redirecting user to new component with React router DOM (am I doing it in right way or not?)
based on that I then use window.location.pathname to add some specific stylings to some components)
Also, I read the entirety of this and this but I could not solve this issue. What am I doing wrong?
withRouter gives you the closest <Route>'s route props, and since the Navigation component is not inside a Route you will not get the route props.
You could e.g. put the Navigation component on a Route outside of the Switch that will always be visible.
Example
class App extends React.Component {
render() {
return (
<Router>
<Fragment>
<Route path="/" component={Navigation} />
<Switch>
<Route exact path="/" component={HomePage} />
<Route exact path="/search" component={HomePage} />
<Route component={Lost} />
</Switch>
</Fragment>
</Router>
);
}
}
My app has the structure below and I would like to pass a prop based in the Header state to the RT component. I can pass it easily with Context but I need to call an API when the prop is a certain value and Context doesn't seem to be designed for this use due to it using the render pattern.
In Header.js I render the children with this.props.children. To pass props I've tried the following patterns but nothing is working. I'm missing a concept here. What is it?
(1) React.Children.map(children, child =>
React.cloneElement(child, { doSomething: this.doSomething }));
(2) {React.cloneElement(this.props.children, { loggedIn: this.state.loggedIn })}
(3) <Route
path="/issues"
render={({ staticContext, ...props }) => <RT {...props} />}
/>
Structure:
App.js
<Header>
<Main />
</Header>
Main.js
const Main = () => (
<Grid item xl={10} lg={10}>
<main>
<Switch>
<Route exact path="/" component={RT} />
<Route path="/projects" component={Projects} />
<Route path="/issues" component={RT}/>
<Route path="/notes" component={Notes} />
</Switch>
</main>
</Grid>
);
I would personally recommend using the React Context API to handle the user state rather than manually passing it via props. Here's an example of how I use it:
import React from 'react';
export const UserContext = React.createContext();
export class UserProvider extends React.Component {
constructor(props) {
super(props);
this.state = {
user: false,
onLogin: this.login,
onLogout: this.logout,
};
}
componentWillMount() {
const user = getCurrentUser(); // pseudo code, fetch the current user session
this.setState({user})
}
render() {
return (
<UserContext.Provider value={this.state}>
{this.props.children}
</UserContext.Provider>
)
}
login = () => {
const user = logUserIn(); // pseudo code, log the user in
this.setState({user})
}
logout = () => {
// handle logout
this.setState({user: false});
}
}
Then you can use the User context wherever you need it like this:
<UserContext.Consumer>
{({user}) => (
// do something with the user state
)}
</UserContext.Consumer>
I created a Meteor createContainer to wrap the entire App to just look for Meteor.users():
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import App from './../App.js';
export default AccountContainer = createContainer(() => {
const _id = Meteor.userId();
const accountHandle = Meteor.subscribe('userData', _id);
const loading = !accountHandle.ready();
const user = Meteor.users.findOne(_id);
const userExist = !loading && !!user;
return {
loading,
userExist,
user
};
}, App);
I need to use this data all around the App to show / hide elements and to take control of User private pages like a control panel.
I'm using Redux to store the data coming from createContainer and actually I'm doing it in the App.js file:
// a few imports here...
class App extends Component {
render() {
const { loading, userExist, user } = this.props;
this.props.updateUser({ loading, userExist, user });
return (
<Router history={history}>
<div className="app">
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} />
<Route exact path="/account" component={Account} />
<Route exact path="/account/pictures" component={Pictures} />
</Switch>
<Newsletter />
<Footer />
</div>
</Router>
);
}
};
export default connect(null, { updateUser })(App);
As you can see here I call this.props.updateUser({ loading, userExist, user }); and it update my redux store.
The issue is that I do receive the following error:
Warning: setState(...): Cannot update during an existing state
transition (such as within render or another component's
constructor). Render methods should be a pure function of props and
state; constructor side-effects are an anti-pattern, but can be moved
to componentWillMount.
This because I'm updating Redux state within the render() App method.
I tried wrapping it between a setTimeout:
setTimeout(() => {
this.props.updateUser({ loading, userExist, user });
});
This solve my issue but I think it is not the best solution to fix it.
I'm confused about how and when to update Redux state using Meteor createContainer.
What I wish to achieve is to be able share the { loading, userExist, user } data all around the App taking that information using for ie in Account.js:
const mapStateToProps = (state) => {
return state.user;
}
export default connect(mapStateToProps)(Account);
Thanks.
EDIT:
Just moved the Redux update frm render() to componentDidMount() method.
It doesn't show the error anymore, it update well in App.js but in Account.js I don't get it updated, it looks like it miss it:
I have (e.g.) two components in React. The first, app.js, is the root component. It imports some JSON data and puts it in its state. This works fine (I can see it in the React devtools).
import data from '../data/docs.json';
class App extends Component {
constructor() {
super();
this.state = {
docs: {}
};
}
componentWillMount() {
this.setState({
docs: data
});
}
render() {
return (
<Router history={hashHistory}>
<Route path="/" component={Wrapper}>
<IndexRoute component={Home} />
<Route path="/home" component={Home} />
<Route path="/docs" component={Docs} />
</Route>
</Router>
);
}
}
The second, docs.js, is meant to show this JSON data. To do that it needs to access the state of app.js. At the moment it errors, and I know why (this does not include app.js). But how then can I pass the state from app.js to docs.js?
class Docs extends React.Component {
render() {
return(
<div>
{this.state.docs.map(function(study, key) {
return <p>Random text here</p>;
})}
</div>
)
}
}
The proper way of doing this would be by passing state as props to Docs component.
However, because you are using React Router it can be accessed in a bit different way: this.props.route.param instead of default this.props.param
So your code should look more or less like this:
<Route path="/docs" component={Docs} docs={this.state.docs} />
and
{this.props.route.docs.map(function(study, key) {
return <p>Random text here</p>;
})}
Another way of doing this is:
<Route path="/docs" component={() => <Docs docs={this.state.docs}/>}>
If you need to pass children:
<Route path="/" component={(props) => <Docs docs={this.state.docs}>{props.children}</Docs>}>
If you are doing it like this, then you can access your props values directly by this.props.docs in Child Component:
{
this.props.docs.map((study, key)=> {
return <p key={key}>Random text here</p>;
})
}
Another way of doing this will be
<Route path='/' render={ routeProps => <Home
{...routeProps}
docs={this.state.docs}
/>
}
/>
While in the child component you can access docs using
this.props.docs
Hope it helps!