I'm trying to implement firebase authentication with google to my app. And I use context to share data between screens. But I ran into problem of too many re-render.
export const UserContext = createContext({user: null});
class UserProvider extends Component {
constructor() {
super();
this.state = {
user: null,
};
}
componentDidMount = () => {
auth.onAuthStateChanged((userAuth) => {
if (!!userAuth) {
console.log("signed in");
console.log(userAuth);
this.setState({user: userAuth});
} else {
console.log("not signed in");
}
});
};
render() {
return (
<UserContext.Provider value={this.state.user}>
{this.props.children}
</UserContext.Provider>
);
}
}
export default UserProvider;
Here's the Application.jsx
return (
<>
<NavBar onHamburgerClick={handleHamburgerClick} showHamburger={true} />
<main>
<LeftMenu visible={leftMenuVisible} showScheduler={true} />
<Router className="main-container">
<Home path="/" />
<NewTimeTable path="new" />
<TeacherTimeTable path="teacher-time-table" />
<AllocateManagement path="allocate-manage" />
<ScheduleTimeTable path="schedule-timetable" />
</Router>
</main>
</>
);
And here's the App.js
const App = () => {
return (
<UserProvider>
<Application />
</UserProvider>
);
};
Also I'm using #reach/router
The problem here I think is because of setState I put inside componentDidMount. The variable userAuth is totally fine. I just can't set it to this.state.user
I don't know what exactly happened but my app works fine now.
export const UserContext = createContext({
user: null,
});
class UserProvider extends Component {
constructor() {
super();
this.state = {
user: {},
};
// set new user
this.setUser = (newUser) => {
this.setState({user: newUser}, () => {
console.log("new user: ");
console.log(this.state.user);
});
};
}
componentDidMount = () => {
// check if the user has changed and setState to the {user}
auth.onAuthStateChanged((userAuth) => {
if (!!userAuth) {
console.log("signed in");
this.setUser(userAuth);
} else {
console.log("not signed in");
}
});
};
render() {
return (
<UserContext.Provider value={this.state.user}>
{this.props.children}
</UserContext.Provider>
);
}
}
export default UserProvider;
I tweak something and it worked! Trying to figure it out
Related
I want to pass a function to a component through a Route , I can do it with direct children but when it comes to Routes i can't figure it how to do it.
look at the code below , i want to pass "updateUserState" function to Profile Component , the function works properly in Header component but it's not working in Profile component which lives inside the Routes .
class App extends React.Component {
updateUserState = (currentUser) => {
if(currentUser != null) {
this.setState({
currentUser: currentUser
})
} else {
this.setState({
currentUser: null
});
}
return this.state.currentUser;
}
render() {
return (
<div className="App">
<Header updateUserState={this.updateUserState} />
<Routes>
<Route path='/profile' element={<ProfilePage updateUserState={this.updateUserState} />}/>
</Routes>
</div>
);
}
}
this is the code in Profile component which is completely similar to Header Component :
const ProfileCard = ({updateUserState}) => {
const signout = () => {
handleLogout()
updateUserState()
}
return (
<div className='profile-card'>
<a onClick={() => signout()}>
Sign Out
</a>
</div>
)
}
Update :
solved thanks to Abir Taheer !
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
currentUser: null
}
this.updateUserState = this.updateUserState.bind(this);
}
updateUserState = (currentUser) => {
if(currentUser != null) {
this.setState({
currentUser: currentUser
}, () => console.log(this.state.currentUser))
} else {
this.setState({
currentUser: null
}, () => console.log(this.state.currentUser));
}
return this.state.currentUser;
}
render() {
return (
<div className="App">
<Header currentUser={this.state.currentUser} updateUserState={this.updateUserState} />
<Routes>
<Route path='/profile' element={<ProfilePage updateUserState={this.updateUserState}
currentUser={this.state.currentUser} />}
/>
</Routes>
</div>
);
}
}
then inside ProfilePage :
const ProfilePage = ( {currentUser, updateUserState} ) => {
return (
<div>{
currentUser ?
<div>
<ProfileCard id={currentUser.id} updateUserState={updateUserState} />
</div>
:
<h1>No User Signed In</h1>
}</div>
)
}
And ProfileCard :
const ProfileCard = ({id, updateUserState}) => {
const signout = () => {
handleLogout()
updateUserState();
}
return (
<div className='profile-card'>
<a onClick={() => signout()}>
Sign Out
</a>
</div>
)
}
The issue arises because of the this keyword. When you're passing a function to another component you need to bind the this keyword to the parent instance otherwise it may not work properly.
This behavior is described in the React Docs here: https://reactjs.org/docs/faq-functions.html
and more specifically further down in the page here: Why is binding necessary at all?
When you bind this to the parent instance then it refers to the correct state and the function should work.
You need to update your component like such:
class App extends React.Component {
constructor(props) {
super(props);
// Make sure to initialize your state accordingly
this.state = {
currentUser: null,
};
// --- This is the line you need ---
this.updateUserState = this.updateUserState.bind(this);
}
updateUserState(currentUser) {
if (currentUser != null) {
this.setState({
currentUser: currentUser,
});
} else {
this.setState({
currentUser: null,
});
}
return this.state.currentUser;
}
render() {
return (
<div className="App">
<Header updateUserState={this.updateUserState} />
<Routes>
<Route
path="/profile"
element={<ProfilePage updateUserState={this.updateUserState} />}
/>
</Routes>
</div>
);
}
}
The way you do it seems like you are rendering the component instead of passing a reference.
How I would suggest is to wrap the component in another function and return with your function passed in as a prop. So basically making another react component with the method passed in. Use that wrapper component instead:
const wrappedProfilePage = () => <ProfilePage updateUserState={this.updateUserState} />;
..
.
.
<Route path='/profile' element={wrappedProfilePage}/>
I am using React with Redux to list number of items and inside the item I have a list of similar items
In Home Page (there is a list of items when you click on any of them , it goes to the item path ) which is working well , but inside the item page , when you click on any items from similar items list (the view not updating )
the codeSandobx is here
App.js
const store = createStore(ItemsReducer, applyMiddleware(...middlewares));
class App extends React.Component {
render() {
return (
<Provider store={store}>
<Main />
</Provider>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
main.js
const Main = () => {
return (
<Router>
<div>
<Header />
<div className="container-fluid">
<Switch>
<Route exact path="/" component={Home} />
<Route path="/item/:id" component={Item} />
</Switch>
</div>
</div>
</Router>
);
};
export default Main;
Home.js
class Home extends React.Component {
render() {
const itemsList = this.props.items.map(item => {
return <ItemList item={item} key={item.id} />;
});
return <div className="items-list"> {itemsList}</div>;
}
}
const mapStateToProps = state => ({
items: state.items,
user: state.user
});
export default connect(mapStateToProps, null, null, {
pure: false
})(Home);
Item.js
class Item extends React.Component {
constructor(props) {
super();
this.state = {
item_id: props.match.params.id,
};
}
render() {
const itemsList = this.props.items.map(item => {
return <ItemList item={item} key={item.id} />;
});
return (
<div id="item-container">
<div className="item-list fav-items"> {itemsList} </div>;
</div>
);
}
}
const mapStateToProps = state => ({
items: state.items,
user: state.user
});
export default connect(mapStateToProps, null, null, {
pure: false
})(Item);
and finally the ItemList.js
class ItemList extends React.Component {
render() {
const item = this.props.item;
const item_link = "/item/" + item.id;
return (
<Link to={item_link}>
<div className="item-li">
{item.title}
</div>
</Link>
);
}
}
export default ItemList;
I've tired to use this solution from react-redux docs , but it didn't work
What do you expect to update on link click?
Any path /item/:id (with any id: 2423, 2435, 5465) will show the same result, because you don't use params.id inside the Item component
UPDATED
When id changes the component doesn't remount, only updates component (It's correct behavior)
If you want to fetchData on each changes of id, the next solution has to work for you
on hooks:
const Item = () => {
const params = useParams();
useEffect(() => {
axios.get(`/item/${params.id}`).then(...)
}, [params.id]);
return (
...
)
}
useEffect will call fetch each time when id is changing
and in class component you have to use componentDidUpdate:
class Item extends Component {
componentDidMount() {
this.fetchData();
}
componentDidUpdate(prevProps) {
if (prevProps.match.params.id !== this.props.match.params.id) {
this.fetchData();
}
}
fetchData = () => {
...
}
...
}
I want to hide some component based on some flag in react js.
I have an App component where I have Login and other components, I want to hide the other component until Login components this.state.success is false and on click of a button I am changing the sate, but it's not working, I am new to react,
My App Class compoenent -
import React, { Component } from "react";
import logo from "../../logo.svg";
// import Game from "../Game/Game";
import Table from "../Table/Table";
import Form from "../Table/Form";
import Clock from "../Clock/Clock";
import "./App.css";
import Login from "../Login/Login";
class App extends Component {
state = {
success: false
};
removeCharacter = index => {
const { characters } = this.state;
this.setState({
characters: characters.filter((character, i) => {
return i !== index;
})
});
};
handleSubmit = character => {
this.setState({ characters: [...this.state.characters, character] });
};
handleSuccess() {
this.setState({ success: true });
}
render() {
const { characters, success } = this.state;
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<span className="Span-inline">App</span>
<Clock time={new Date()} />
</header>
<Login success={success} handleSuccess={this.handleSuccess} />
{success && (
<div className="container">
<h1>React Tutorial</h1>
<p>Add a character with a name and a job to the table.</p>
<Table
characterData={characters}
removeCharacter={this.removeCharacter}
/>
<h3>Add New character</h3>
<Form handleSubmit={this.handleSubmit} />
</div>
)}
{/* <Game /> */}
</div>
);
}
}
export default App;
My Login component -
import React, { Component } from "react";
import Greeting from "./Greeting";
import LogoutButton from "./LogoutButton";
import LoginButton from "./LoginButton";
class Login extends Component {
constructor(props) {
super(props);
this.handleLoginClick = this.handleLoginClick.bind(this);
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.state = {
isLoggedIn: false,
name: "",
success: false
};
}
handleLoginClick() {
this.setState({ isLoggedIn: true });
this.setState({ success: true });
}
handleLogoutClick() {
this.setState({ isLoggedIn: false });
this.setState({ success: false });
}
onChange = e => {
this.setState({
name: e.target.value
});
};
render() {
const isLoggedIn = this.state.isLoggedIn;
const name = this.state.name;
// const successLogin = this.state.success;
let button;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}
return (
<div>
<Greeting
isLoggedIn={isLoggedIn}
name={name}
onChange={this.onChange}
/>
{button}
</div>
);
}
}
export default Login;
please guide me on what I am doing wrong.
Why sometime debuggers do not trigger in react component?
For the sake of example I have used functional stateless component here. You can use Class component all upto you.
const conditionalComponent = (props) => {
let condition = true;
return (
{condition && <div><h1>Hello world</h1></div>}
}
Instead of directly giving condition you can even call function which returns a boolean value.
handleLoginClick() {
this.setState({ isLoggedIn: true });
this.setState({ success: true });
this.props.handleSuccess()
}
do like this
<Login success={success} handleSuccess=
{this.handleSuccess} />
bind this function
I am new in React and I will appreaciate much any help. I am using create-react-app, react-router-dom and express server. When I try to submit a comment to a blog post (child component called Details), it gets stored in the database, however the component does not seem to update and i do not see the new comment.As a result, I can see the new comment only after i refresh the page but not on form submit. I guess I am not setting componentDidUpdate properly but I do not have a clue how to do it, so i can see the comment immediately.
Here is my App.js:
class App extends Component {
constructor(props) {
super(props)
this.state = {
userId: null,
username: null,
isAdmin: false,
isAuthed: false,
jwtoken: null,
posts: [],
filtered: [],
}
this.handleSubmit = this.handleSubmit.bind(this)
}
static authService = new AuthService();
static postService = new PostService();
static commentService = new CommentService();
componentDidMount() {
const isAdmin = localStorage.getItem('isAdmin') === "true"
const isAuthed = !!localStorage.getItem('username');
if (isAuthed) {
this.setState({
userId: localStorage.getItem('userId'),
username: localStorage.getItem('username'),
isAdmin,
isAuthed,
})
}
this.getPosts()
}
componentDidUpdate(prevProps, prevState, posts) {
if (prevState === this.state) {
this.getPosts()
}
}
handleChange(e, data) {
this.setState({
[e.target.name]: e.target.value
})
}
handleCommentSubmit(e, data) {
e.preventDefault();
e.target.reset();
App.commentService.createComment(data)
.then(body => {
this.getposts()
if (!body.errors) {
toast.success(body.message);
}
else {
toast.error(body.message);
}
}
)
.catch(error => console.error(error));
}
getPosts() {
App.postService.getPost()
.then(data => {
this.setState({
posts: data.posts.length? data.posts : []
});
}
)
.catch(e => this.setState({ e }))
}
render() {
return (
<Fragment>
<Header username={this.state.username} isAdmin={this.state.isAdmin} isAuthed={this.state.isAuthed} logout={this.logout.bind(this)} />
<Switch>
<Route exact path="/" render={(props) => (
<Home
posts={this.state.posts}
handleSearchSubmit={this.handleSearchSubmit.bind(this)}
handleChange={this.handleSearchChange.bind(this)}
{...props} />
)} />
<Route path="/posts/:id" render={(props) =>
<Details handleSubmit={this.handleCommentSubmit.bind(this)}
isAdmin={this.state.isAdmin}
isAuthed={this.state.isAuthed}
posts={this.state.posts}
handleChange={this.handleChange}
{...props} />} />
</Switch>
<Footer posts={this.state.posts} formatDate={this.formatDate} />
</Fragment>
);
}
}
export default withRouter(App);
Here is my Details.js:
class Details extends Component {
constructor(props) {
super(props);
this.state = {
post: null,
comment: null
}
this.handleChange = props.handleChange.bind(this);
}
componentDidMount() {
const { posts, match } = this.props;
this.setState({
post: posts.length
? posts.find(p => p._id === match.params.id)
: null,
userId: localStorage.getItem('userId')
})
}
componentDidUpdate(prevProps) {
const { posts, match, isAuthed } = this.props;
if (JSON.stringify(prevProps) === JSON.stringify(this.props)) {
return;
}
this.setState({
post: posts.length
? posts.find(p => p._id === match.params.id)
: null
});
}
render() {
const { post } = this.state;
const { isAdmin, isAuthed } = this.props;
if (!post) {
return <span>Loading post ...</span>;
}
return (
<section className="site-section py-lg">
<form onSubmit={(e)=> this.props.handleSubmit(e, this.state)} className="p-5 bg-light">
<div className="form-group">
<label htmlFor="message">Message</label>
<textarea name="comment" id="message" onChange={this.handleChange} cols={30} rows={10} className="form-control" defaultValue={ ""} />
</div>
<div className="form-group">
<input type="submit" defaultValue="Post Comment" className="btn btn-primary" />
</div>
</form>}
</section>
);
}
}
export default Details;
Any help will be much appreciated!
You are doing a mistake that will be done by any new React developer. Just remember one thing that:-
UI is a function of state
So your UI will only be updated if your state is update.
After submitting a comment don't fetch all your comments again, just concat your new comment to current state and you will see your comment as soon as you submit it successfully
I have a function that render LoginPage if the user is not logged and render the IndexPage if is logged, but It is not rendering none, I tried alerting the user.displayName and It work. See my code.
renderPage = () => {
firebase.auth().onAuthStateChanged(user => {
if (user) {
return <IndexPage />;
} else {
return <LoginPage />;
}
});
};
render() {
return <div>{this.renderPage()}</div>;
}
Why is not working?
You miss a return in the renderPage function, but performing async requests in render is not a good approach in react.
What you should do, is to move the user into the state, then on componentDidMount fetch the user from your async code, and inside your render use the state prop user.
So your code should be something like:
constructor() {
this.state = { user: null };
}
componentDidMount() {
firebase.auth().onAuthStateChanged(user => {
user ? this.setState({ user }) : this.setState({ user: null });
});
}
render() {
const content = this.state.user ? <IndexPage /> : <LoginPage />;
return <div>{content}</div>;
}
Your function inside render method is async function, what you get is undefined.
You should store the user state. Do something like,
class YourComponent extends Component {
constructor(props) {
super(props);
this.state = {
user: null
};
}
componentDidMount() {
firebase.auth().onAuthStateChanged(user => {
if (user) {
this.setState({
user
});
}
});
}
render() {
return (
{this.state.user ? <IndexPage /> : <LoginPage />}
);
}
}