React, Firebase: Auth possibly failing on redirect - reactjs

I've got component that captures user information and posts it to firebase, when the submit button is hit. Also within this component is a function that gets all posted data, and returns only the posts made by the currently logged in user:
authListener() {
auth().onAuthStateChanged(user => {
if(user){
this.setState({
userDetails:user
},
() =>
firebase.firestore().collection('gig-listing').onSnapshot(querySnapshot => {
let filteredGigs = querySnapshot.docs.filter(snapshot => {
return snapshot.data().user === this.state.userDetails.uid
})
this.setState({
filterGigs: filteredGigs
})
})
) //end of set state
} else {
this.setState({
userDetails:null
})
console.log('no user signed in')
}
})
}
This function works and displays what it should. However, when the submit function (in the same component) is executed, it should redirect to another page, but instead I get no render, and the error
TypeError: Cannot read property 'uid' of null
When I refresh the page however, it shows the correct page.
What this is basically saying is that, on redirect, this.state.userDetails.uid evaluates to null, even though I'm signed in at the time. Any ideas why this is happening and any potential solutions?
Here's the component in it's entirety:
import React from "react";
import Header from "./Header";
import TextField from "#material-ui/core/TextField";
import Button from "#material-ui/core/Button";
import { withStyles } from '#material-ui/core/styles';
import axios from "axios";
import firebase from 'firebase'
import { auth } from 'firebase/app'
import {Link} from 'react-router-dom'
import UniqueVenueListing from './UniqueVenueListing'
const StyledButton = withStyles({
root: {
background: '#54ADA6',
borderRadius: 3,
border: 0,
color: 'white',
height: 30,
padding: '0 30px',
marginRight: '1px'
},
label: {
textTransform: 'capitalize',
},
})(Button);
class GigRegister extends React.Component {
constructor() {
super();
this.state = {
name: "",
venue: "",
time: "",
date: "",
genre: "",
tickets: "",
price: "",
venueWebsite: "",
bandWebsite:"",
userDetails: {},
filterGigs: [],
isLoggedIn:false,
currentToken:{}
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleChange(e) {
this.setState({
[e.target.name]: e.target.value,
});
}
handleClick() {
console.log("handle click reached");
auth()
.signOut()
.then(() => {
console.log("Successfully signed out");
})
.catch((err) => {
console.log(err);
});
}
authListener() {
auth().onAuthStateChanged(user => {
if(user){
console.log(`this is the user: ${user.uid}`)
this.setState({
userDetails:user
},
() =>
firebase.firestore().collection('gig-listing').onSnapshot(querySnapshot => {
let filteredGigs = querySnapshot.docs.filter(snapshot => {
return snapshot.data().user === this.state.userDetails.uid
})
this.setState({
filterGigs: filteredGigs
})
})
) //end of set state
} else {
this.setState({
userDetails:null
})
console.log('no user signed in')
}
})
}
componentDidMount() {
this.authListener();
}
handleSubmit(e) {
let user = this.state.userDetails.uid;
const gigData = {
name: this.state.name,
venue: this.state.venue,
time: this.state.time,
date: this.state.date,
genre: this.state.genre,
tickets: this.state.tickets,
price: this.state.price,
venueWebsite: this.state.venueWebsite,
bandWebsite: this.state.bandWebsite,
user: user
};
auth()
.currentUser.getIdToken()
.then(function (token) {
axios(
"https://us-central1-gig-fort.cloudfunctions.net/api/createGigListing",
{
method: "POST",
headers: {
"content-type": "application/json",
Authorization: "Bearer " + token,
},
data: gigData,
}
);
})
.then((res) => {
this.props.history.push("/Homepage");
})
.catch((err) => {
console.error(err);
});
}
render() {
return (
<div className="gig-register">
<Header />
<div className="heading-container">
<h1>Venue Dashboard</h1> <br></br>
{this.state.userDetails ? (
<h3>You are signed in as {this.state.userDetails.email}</h3>
) : null}
<div className="gig-reg-buttons">
{this.state.userDetails ? (
<StyledButton onClick={this.handleClick}>Sign out </StyledButton>
) : (
<Link to="/" style={{ textDecoration: "none" }}>
<StyledButton>Sign In</StyledButton>
</Link>
)}
<Link to="/Homepage" style={{ textDecoration: "none" }}>
<StyledButton>Go to gig listings</StyledButton>
</Link>
</div>
</div>
<div className="handle-gigs">
<div className="reg-gig-input">
<form onSubmit={this.handleSubmit}>
<h3>Register a gig</h3>
<br></br>
<TextField
placeholder="Event name"
id="name"
name="name"
onChange={this.handleChange}
/>
<TextField
placeholder="Time"
type="time"
label="Enter start time"
id="time"
name="time"
InputLabelProps={{
shrink: true,
}}
inputProps={{
step: 300, // 5 min
}}
onChange={this.handleChange}
/>
<TextField
id="date"
label="Select date"
type="date"
InputLabelProps={{
shrink: true,
}}
onChange={(e) => {
this.setState({ date: e.target.value });
}}
/>
<TextField
placeholder="Genre"
id="genre"
name="genre"
onChange={this.handleChange}
/>
<TextField
placeholder="Band website"
id="bandWebsite"
name="bandWebsite"
onChange={this.handleChange}
/>
<TextField
placeholder= "Link to ticketing agent"
id="tickets"
name="tickets"
onChange={this.handleChange}
/>
<TextField
placeholder="Price"
id="price"
name="price"
onChange={this.handleChange}
/>
<TextField
placeholder="Venue website"
id="venueWebsite"
name="venueWebsite"
onChange={this.handleChange}
/>
<Button type="submit">Submit</Button>
</form>
</div>
<div className="manage-gigs">
<h3 className="manage-gig">Manage your gigs</h3>
<br></br>
{this.state.userDetails ? (
<UniqueVenueListing gigList={this.state.filterGigs} />
) : (
<h2>no gigs to show</h2>
)}
</div>
</div>
</div>
);
}
}
export default GigRegister

Since the data is loaded from Firestore asynchronously, there is no guarantee that the user is still signed in by the time your filter method runs.
If you want to continue using the user that was signed in when the onAuthStateChanged triggered, use the user parameter that was passed in, instead of the value from the state:
if(user){
console.log(`this is the user: ${user.uid}`)
this.setState({
userDetails:user
},
() =>
firebase.firestore().collection('gig-listing').onSnapshot(querySnapshot => {
let filteredGigs = querySnapshot.docs.filter(snapshot => {
return snapshot.data().user === user.uid
})
The user variable in here is guaranteed to still have the correct value, as it is defined within the same scope. The state doesn't have such a guarantee as it is globally shared.
Alternatively if you want to use the user from the state, check whether it still has a value once you get the data from the database:
if(user){
console.log(`this is the user: ${user.uid}`)
this.setState({
userDetails:user
},
() =>
firebase.firestore().collection('gig-listing').onSnapshot(querySnapshot => {
if (this.state.userDetails) {
let filteredGigs = querySnapshot.docs.filter(snapshot => {
return snapshot.data().user === this.state.userDetails.uid
})
}
else {
console.log("User no longer signed in when database returned results, skipping filtering...");
}

Related

Firebase, React: Axios GET request inside onAuthStateChanged causing rendering problems

Within my GigRegister component, I have an auth listener checking for auth status, and within that auth listener, I have a axios GET request fetching data that is then filtered through, and set to state:
authListener() {
auth().onAuthStateChanged((user) => {
if (user) {
this.setState({
userDetails: user,
isLoggedIn: true
});
axios
.get(
"https://us-central1-gig-fort.cloudfunctions.net/api/getGigListings"
)
.then((res) => {
let filteredGigs = res.data.filter((gig) => {
return gig.user === this.state.userDetails.uid;
});
this.setState({
filterGigs: filteredGigs,
});
});
} else {
this.setState({
userDetails: null,
});
console.log("no user signed in");
}
});
}
componentDidMount() {
this.authListener();
}
However, this is causing some issue with how to Gig Regiester page loads - it doesn't load the first time, but it does when the page is refreshed. Here's the initial error message:
Unhandled Rejection (TypeError): Cannot read property 'uid' of null
....and here's the entire component:
import React from "react";
import Header from "./Header";
import TextField from "#material-ui/core/TextField";
import Button from "#material-ui/core/Button";
import axios from "axios";
import * as firebase from 'firebase'
import { auth } from 'firebase/app'
import {Link} from 'react-router-dom'
import UniqueVenueListing from './UniqueVenueListing'
class GigRegister extends React.Component {
constructor() {
super();
this.state = {
name: "",
venue: "",
time: "",
date: "",
genre: "",
tickets: "",
price: "",
userDetails:{},
filterGigs:[]
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this)
}
handleChange(e) {
this.setState({
[e.target.name]: e.target.value,
});
}
handleClick(){
console.log('handle click reached')
auth().signOut().then(() => {
console.log('Successfully signed out')
})
.catch(err => {
console.log(err)
})
}
authListener(){
auth().onAuthStateChanged((user)=>{
if(user){
this.setState({
userDetails: user
})
axios.get("https://us-central1-gig-fort.cloudfunctions.net/api/getGigListings")
.then(res=> {
let filteredGigs = res.data
.filter(gig => {
return gig.user === this.state.userDetails.uid
})
this.setState({
filterGigs: filteredGigs
})
})
} else {
this.setState({
userDetails: null
})
console.log('no user signed in')
}
})
}
componentDidMount(){
this.authListener()
}
handleSubmit(e) {
let user = auth().currentUser.uid
const gigData = {
name: this.state.name,
venue: this.state.venue,
time: this.state.time,
date: this.state.date,
genre: this.state.genre,
tickets: this.state.tickets,
price: this.state.price,
user:user
};
auth().currentUser.getIdToken().then(function(token) {
axios("http://localhost:5000/gig-fort/us-central1/api/createGigListing", {
method: "POST",
headers: {
"content-type": "application/json",
"Authorization": "Bearer "+token,
},
data: gigData,
})
})
.then((res) => {
console.log(res);
this.props.history.push('/Homepage')
})
.catch((err) => {
console.error(err);
});
}
render() {
return (
<div className="gig-register">
<Header />
<div className = 'heading-container'>
<h1>Venue Dashboard</h1> <br></br>
{
this.state.userDetails ?
<h3>You are signed in as {this.state.userDetails.email}</h3>
:
null
}
<div className = 'gig-reg-buttons'>
{
this.state.userDetails ?
<Button onClick = {this.handleClick}>Sign out </Button>
:
<Link to = '/' style={{ textDecoration: "none" }}>
<Button>Sign In</Button>
</Link>
}
<Link to="/Homepage" style={{ textDecoration: "none" }}>
<Button>Go to gig listings</Button>
</Link>
</div>
</div>
<div className = 'handle-gigs'>
<div className = 'reg-gig-input'>
<form onSubmit={this.handleSubmit}>
<h3>Register a gig</h3>
<br></br>
<TextField
placeholder="Event name"
defaultValue="Event name"
id="name"
name="name"
onChange={this.handleChange}
/>
<TextField
placeholder="Time"
defaultValue="Time"
type="time"
label="Enter start time"
id="time"
name="time"
InputLabelProps={{
shrink: true,
}}
inputProps={{
step: 300, // 5 min
}}
onChange={this.handleChange}
/>
<TextField
id="date"
label="Select date"
type="date"
defaultValue="2017-05-24"
InputLabelProps={{
shrink: true,
}}
onChange={(e) => {
this.setState({ date: e.target.value });
}}
/>
<TextField
placeholder="Genre"
defaultValue="Genre"
id="genre"
name="genre"
onChange={this.handleChange}
/>
<TextField
placeholder="Tickets"
defaultValue="Tickets"
id="tickets"
name="tickets"
onChange={this.handleChange}
/>
<TextField
placeholder="Price"
defaultValue="Price"
id="price"
name="price"
onChange={this.handleChange}
/>
<Button type="submit">Submit</Button>
</form>
</div>
<div className = 'manage-gigs'>
<h3 className = 'manage-gig'>Manage your gigs</h3>
<br></br>
{ this.state.userDetails ?
<UniqueVenueListing gigList = {this.state.filterGigs}/>
:
<h2>no gigs to show</h2>
}
</div>
</div>
</div>
);
}
}
export default GigRegister
Can anyone suggest a solution so that the GigRegister component loads properly on the first time? I should mention that GigRegister is directed to from the login component, upon successful login.
You are trying to do multiple dependent state changes in one batch
This part needs to be split in 2
authListener(){
auth().onAuthStateChanged((user)=>{
if(user){
this.setState({
userDetails: user
})
axios.get("https://us-central1-gig-fort.cloudfunctions.net/api/getGigListings")
.then(res=> {
let filteredGigs = res.data
.filter(gig => {
return gig.user === this.state.userDetails.uid
})
this.setState({
filterGigs: filteredGigs
})
})
} else {
this.setState({
userDetails: null
})
console.log('no user signed in')
}
})
}
Since you set userDetails and then try to use it in one "batch". Put the data request in a componentDidUpdate or useEffect. I recommend only using functional components with hooks.
React.useEffect(() => {
if (state.userDetails) {
/* data request */
}
}, [userDetails])
But since you use a class you need component did update.
componentDidUpdate() {
if (this.state.userDetails && !this.state.filterGigs) {
/* data request */
}
}
This way your component responds to getting data when it has rerendered with its new props.

Firebase, React: How do I make authenticated POST requests, without having tokens expire?

I've got a react component controlling authorized posts to my firestore. Users submit text-based information, and it's sent to firebase using axios as a client. Only problem is that when I try to submit data, I get the following error:
Firebase ID token has expired. Get a fresh ID token from your client app and try again".
I suspect that on the client-side, getIdToken() is sending an expired token and I'm not sure how to fix it. I tried setting it to getIdToken(true) which didn't work either.
The only "solution" so far has been changing my laptop time to match the timezone of the server, which obviously isn't the fix I'm after.
So, I just want logged in users to be able to make a post without hitting this error, would appreciate any solutions/suggestions.
Here's the axios post within the GigRegister component.
handleSubmit(e) {
let user = auth().currentUser.uid;
const gigData = {
name: this.state.name,
venue: this.state.venue,
time: this.state.time,
date: this.state.date,
genre: this.state.genre,
tickets: this.state.tickets,
price: this.state.price,
user: user
};
auth()
.currentUser.getIdToken()
.then(function (token) {
axios(
"https://us-central1-gig-fort.cloudfunctions.net/api/createGigListing",
{
method: "POST",
headers: {
"content-type": "application/json",
Authorization: "Bearer " + token,
},
data: gigData,
}
);
})
.then((res) => {
this.props.history.push("/Homepage");
})
.catch((err) => {
console.error(err);
});
}
...and here's the express/firebase functions. Including is the FBauth middleware:
const FBAuth = (req, res, next) => {
let idToken;
if(req.headers.authorization && req.headers.authorization.startsWith('Bearer ')){
idToken = req.headers.authorization.split('Bearer ')[1]
} else {
console.error('No token found')
return res.status(403).json({error: 'Unauthorized'})
}
admin.auth().verifyIdToken(idToken)
.then(decodedToken => {
req.user = decodedToken;
return db.collection('users')
.where('userId', '==',req.user.uid)
.limit(1)
.get()
})
.then(data =>{
req.user.venueName = data.docs[0].data().venueName;
return next();
})
.catch(err => {
console.error('Error while verifying token', err)
return res.status(403).json(err)
})
}
app.post('/createGigListing', FBAuth, (req,res) => {
const newGig = {
venueName: req.user.venueName,
name: req.body.name,
time: req.body.time,
price: req.body.price,
genre: req.body.genre,
tickets: req.body.tickets,
date: req.body.date,
user:req.body.user,
createdAt: new Date().toISOString()
}
db
.collection('gig-listing')
.add(newGig)
.then(doc => {
res.json({message: `document ${doc.id} created successfully`})
})
.catch(err =>{
res.status(500).json({error: 'something went wrong'})
console.error(err)
})
})
In case anyone needs it, here's the entire GigRegister component
import React from "react";
import Header from "./Header";
import TextField from "#material-ui/core/TextField";
import Button from "#material-ui/core/Button";
import axios from "axios";
import * as firebase from 'firebase'
import { auth } from 'firebase/app'
import {Link} from 'react-router-dom'
import UniqueVenueListing from './UniqueVenueListing'
class GigRegister extends React.Component {
constructor() {
super();
this.state = {
name: "",
venue: "",
time: "",
date: "",
genre: "",
tickets: "",
price: "",
userDetails: {},
filterGigs: [],
isLoggedIn:false,
currentToken:{}
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleChange(e) {
this.setState({
[e.target.name]: e.target.value,
});
}
handleClick() {
console.log("handle click reached");
auth()
.signOut()
.then(() => {
console.log("Successfully signed out");
})
.catch((err) => {
console.log(err);
});
}
authListener() {
auth().onAuthStateChanged((user) => {
if (user) {
this.setState(
{
userDetails: user
},
() =>
axios
.get(
"https://us-central1-gig-fort.cloudfunctions.net/api/getGigListings"
)
.then((res) => {
let filteredGigs = res.data.filter((gig) => {
return gig.user === this.state.userDetails.uid;
});
this.setState({
filterGigs: filteredGigs
});
})
);
} else {
this.setState({
userDetails: null,
});
console.log("no user signed in");
}
});
}
componentDidMount() {
this.authListener();
}
handleSubmit(e) {
let user = auth().currentUser.uid;
const gigData = {
name: this.state.name,
venue: this.state.venue,
time: this.state.time,
date: this.state.date,
genre: this.state.genre,
tickets: this.state.tickets,
price: this.state.price,
user: user
};
auth()
.currentUser.getIdToken()
.then(function (token) {
axios(
"https://us-central1-gig-fort.cloudfunctions.net/api/createGigListing",
{
method: "POST",
headers: {
"content-type": "application/json",
Authorization: "Bearer " + token,
},
data: gigData,
}
);
})
.then((res) => {
this.props.history.push("/Homepage");
})
.catch((err) => {
console.error(err);
});
}
render() {
return (
<div className="gig-register">
<Header />
<div className="heading-container">
<h1>Venue Dashboard</h1> <br></br>
{this.state.userDetails ? (
<h3>You are signed in as {this.state.userDetails.email}</h3>
) : null}
<div className="gig-reg-buttons">
{this.state.userDetails ? (
<Button onClick={this.handleClick}>Sign out </Button>
) : (
<Link to="/" style={{ textDecoration: "none" }}>
<Button>Sign In</Button>
</Link>
)}
<Link to="/Homepage" style={{ textDecoration: "none" }}>
<Button>Go to gig listings</Button>
</Link>
</div>
</div>
<div className="handle-gigs">
<div className="reg-gig-input">
<form onSubmit={this.handleSubmit}>
<h3>Register a gig</h3>
<br></br>
<TextField
placeholder="Event name"
defaultValue="Event name"
id="name"
name="name"
onChange={this.handleChange}
/>
<TextField
placeholder="Time"
defaultValue="Time"
type="time"
label="Enter start time"
id="time"
name="time"
InputLabelProps={{
shrink: true,
}}
inputProps={{
step: 300, // 5 min
}}
onChange={this.handleChange}
/>
<TextField
id="date"
label="Select date"
type="date"
defaultValue="2017-05-24"
InputLabelProps={{
shrink: true,
}}
onChange={(e) => {
this.setState({ date: e.target.value });
}}
/>
<TextField
placeholder="Genre"
defaultValue="Genre"
id="genre"
name="genre"
onChange={this.handleChange}
/>
<TextField
placeholder="Tickets"
defaultValue="Tickets"
id="tickets"
name="tickets"
onChange={this.handleChange}
/>
<TextField
placeholder="Price"
defaultValue="Price"
id="price"
name="price"
onChange={this.handleChange}
/>
<Button type="submit">Submit</Button>
</form>
</div>
<div className="manage-gigs">
<h3 className="manage-gig">Manage your gigs</h3>
<br></br>
{this.state.userDetails ? (
<UniqueVenueListing gigList={this.state.filterGigs} />
) : (
<h2>no gigs to show</h2>
)}
</div>
</div>
</div>
);
}
}
export default GigRegister

Title and event form input not posted and retrieve using Reactjs

The Reactjs code below works excellently by posting and retrieving Users newStatus from a form input to and from an API.
Now I want to add two additional form form inputs (newTitle and newEvent) as per code below
<input className="input-status" name="newTitle"
value={this.state.newTitle}
onChange={e => this.handleNewStatusChange(e)}
placeholder="your Title"
/>
<input className="input-status" name="newEvent"
value={this.state.newEvent}
onChange={e => this.handleNewStatusChange(e)}
placeholder="Your Event Centre Name"
/>
I have also implemented newEvent and newTitle variables in code where possible. My issue is that if type in the first form inputs (eg newStatus) it gets replicated or inserted in all other remaining two forms inputs as can be seen in the screenshot below.
And I believe that's why only the newStatus get posted and retrieved.
I have commented out all newEvent and newTitle variables in code so readers can see where I am wrong.
Here is the code profile.jsx:
import React, { Component } from 'react';
import {
isSignInPending,
loadUserData,
Person,
getFile,
putFile,
lookupProfile
} from 'blockstack';
import Status from './Status.jsx';
const avatarFallbackImage = 'https://mysite/onename/avatar-placeholder.png';
const statusFileName = 'statuses.json'
export default class Profile extends Component {
constructor(props) {
super(props);
this.state = {
person: {
name() {
return 'Anonymous';
},
avatarUrl() {
return avatarFallbackImage;
},
},
username: "",
newStatus: "",
//newTitle: "",
// newEvent: "",
statuses: [],
statusIndex: 0,
isLoading: false
};
this.handleDelete = this.handleDelete.bind(this);
this.isLocal = this.isLocal.bind(this);
}
componentDidMount() {
this.fetchData()
}
handleNewStatusChange(event) {
this.setState({
newStatus: event.target.value,
//newTitle: event.target.value,
//newEvent: event.target.value
})
}
handleNewStatusSubmit(event) {
this.saveNewStatus(
this.state.newStatus,
//this.state.newTitle,
//this.state.newEvent
)
this.setState({
newStatus: "",
//newTitle: "",
//newEvent: "",
})
}
handleDelete(id) {
const statuses = this.state.statuses.filter((status) => status.id !== id)
const options = { encrypt: false }
putFile(statusFileName, JSON.stringify(statuses), options)
.then(() => {
this.setState({
statuses
})
})
}
saveNewStatus(statusText) {
let statuses = this.state.statuses
let status = {
id: this.state.statusIndex++,
text: statusText.trim(),
//textTitle: statusText.trim(),
//textEvent: statusText.trim(),
created_at: Date.now()
}
statuses.unshift(status)
const options = { encrypt: false }
putFile(statusFileName, JSON.stringify(statuses), options)
.then(() => {
this.setState({
statuses: statuses
})
})
}
fetchData() {
if (this.isLocal()) {
this.setState({ isLoading: true })
const options = { decrypt: false, zoneFileLookupURL: 'https://myapi/v1/names/' }
getFile(statusFileName, options)
.then((file) => {
var statuses = JSON.parse(file || '[]')
this.setState({
person: new Person(loadUserData().profile),
username: loadUserData().username,
statusIndex: statuses.length,
statuses: statuses,
})
})
.finally(() => {
this.setState({ isLoading: false })
})
} else {
const username = this.props.match.params.username
this.setState({ isLoading: true })
lookupProfile(username)
.then((profile) => {
this.setState({
person: new Person(profile),
username: username
})
})
.catch((error) => {
console.log('could not resolve profile')
})
const options = { username: username, decrypt: false, zoneFileLookupURL: 'https://myapi/v1/names/'}
getFile(statusFileName, options)
.then((file) => {
var statuses = JSON.parse(file || '[]')
this.setState({
statusIndex: statuses.length,
statuses: statuses
})
})
.catch((error) => {
console.log('could not fetch statuses')
})
.finally(() => {
this.setState({ isLoading: false })
})
}
}
isLocal() {
return this.props.match.params.username ? false : true
}
render() {
const { handleSignOut } = this.props;
const { person } = this.state;
const { username } = this.state;
return (
!isSignInPending() && person ?
<div className="container">
<div className="row">
<div className="col-md-offset-3 col-md-6">
<div className="col-md-12">
<div className="avatar-section">
<img
src={ person.avatarUrl() ? person.avatarUrl() : avatarFallbackImage }
className="img-rounded avatar"
id="avatar-image"
/>
<div className="username">
<h1>
<span id="heading-name">{ person.name() ? person.name()
: 'no name found' }</span>
</h1>
<span>{username}</span>
{this.isLocal() &&
<span>
|
<a onClick={ handleSignOut.bind(this) }>(Logout)</a>
</span>
}
</div>
</div>
</div>
{this.isLocal() &&
<div className="new-status">
<div className="col-md-12">
<textarea className="input-status" name="newStatus"
value={this.state.newStatus}
onChange={e => this.handleNewStatusChange(e)}
placeholder="What's on your mind?"
/>
<input className="input-status" name="newTitle"
value={this.state.newTitle}
onChange={e => this.handleNewStatusChange(e)}
placeholder="your Title"
/>
<input className="input-status" name="newEvent"
value={this.state.newEvent}
onChange={e => this.handleNewStatusChange(e)}
placeholder="Your Event Centre Name"
/>
</div>
<div className="col-md-12 text-right">
<button
className="btn btn-primary btn-lg"
onClick={e => this.handleNewStatusSubmit(e)}
>
Submit
</button>
</div>
</div>
}
<div className="col-md-12 statuses">
{this.state.isLoading && <span>Loading...</span>}
{
this.state.statuses.map((status) => (
<Status
key={status.id}
status={status}
handleDelete={this.handleDelete}
isLocal={this.isLocal}
/>
))
}
</div>
</div>
</div>
</div> : null
);
}
}
Here is just sample part for for status.jsx showing how I successfully return the status:
//some coding...
return (
<div className="status">
<div className="status-text">
{status.text}
/*
{status.textTitle} {status.textEvent}
*/
</div>
</div>
)
You can use the name attribute of the input field to your advantage.
And update your change handler function:
handleNewStatusChange(event) {
this.setState({
[event.target.name]: event.target.value,
})
}
more info on { [key]: value }

Reactjs Hiding form clicking on button from another component

I have written a crud application and it works great!
I have successfully implemented when people click on ADD NEW button form will be visible
Now i am facing challange to hide form when people click on SAVE button, coz, the SAVE another component.
Here you go for my Form.js File:
import React, { Fragment } from "react"
import { connect } from 'react-redux'
const axios = require('axios');
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
id: this.props.selectedData.id,
name: this.props.selectedData.name,
age: this.props.selectedData.age,
};
this.onHandleChange = this.onHandleChange.bind(this);
this.submit = this.submit.bind(this);
}
submit(event) {
const data = {
name: this.state.name,
age: this.state.age,
email: this.state.email
};
if (this.props.isEdit) {
data.id = this.props.selectedData.id;
axios.put('http://127.0.0.1:8000/api/v1/employee/' + data.id + '/', data)
.then((response) => {
this.props.dispatch({ type: 'EDIT_POST', response });
});
} else {
// generate id here for new emplyoee
axios.post('http://127.0.0.1:8000/api/v1/employee/', data)
.then((response) => {
this.props.dispatch({ type: 'ADD_POST', response });
});
}
}
onHandleChange(event) {
this.setState({
[event.target.name]: event.target.value
});
}
componentDidUpdate(prevProps) {
if (prevProps.selectedData.age !== this.props.selectedData.age) { //Check on email, because email is unique
this.setState({ name: this.props.selectedData.name, age: this.props.selectedData.age })
}
}
render() {
return (
<form>
<div className="form-group">
<input onChange={(event) => this.onHandleChange(event)} value={this.state.name} name="name" type="text" />
</div>
<div className="form-group">
<input onChange={(event) => this.onHandleChange(event)} value={this.state.age} name="age" type="number" />
</div>
<button onClick={(event) => this.submit(event)} type="button">
{this.props.isEdit ? 'Update' : 'SAVE'}
</button>
</form>
);
}
}
export default connect(null)(Form);
And this is my Home.js file:
import React from "react"
import Table from "../components/table"
import Form from '../components/form'
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedData: {name: '', age: ''},
isEdit: false,
isFormVisible: false,
};
}
selectedData = (item) => {
this.setState({selectedData: item,isEdit:true, isFormVisible: true})
}
render() {
return (
<React.Fragment>
{this.state.isFormVisible && <div>
<Form selectedData={this.state.selectedData} isEdit={this.state.isEdit}/>
</div>}
{!this.state.isFormVisible &&
<button onClick={() => this.setState({isFormVisible: true})}>ADD NEW</button>}
<table>
<Table selectedData={this.selectedData} />
</table>
</React.Fragment>
);
}
}
export default Home;
Everything is working fine, only issue is hiding the form when i click on SAVE button.
Can anyone help to hide the form clicing on SAVE button?
Inside your Home.js, create a function that does the state change for you and pass it down as a prop to the Form Component.
Home.js
changeFormState = () => {
this.setState({ isFormVisible: !isFormVisible });
}
<Form
changeFormState={this.changeFormState}
selectedData={this.state.selectedData}
isEdit={this.state.isEdit}
/>
Form.js
<button
onClick={(event) => { this.props.isEdit ? this.submit(event) :
this.props.changeFormState() }} type="button"
>
{this.props.isEdit ? 'Update' : 'SAVE'}
</button>
JS Update
submit(event) {
const data = {
name: this.state.name,
age: this.state.age,
email: this.state.email
};
if (this.props.isEdit) {
data.id = this.props.selectedData.id;
axios.put('http://127.0.0.1:8000/api/v1/employee/' + data.id + '/', data)
.then((response) => {
// Fire an event
this.props.onSave && this.props.onSave();
this.props.dispatch({ type: 'EDIT_POST', response });
});
} else {
// generate id here for new emplyoee
axios.post('http://127.0.0.1:8000/api/v1/employee/', data)
.then((response) => {
// Fire an event
this.props.onSave && this.props.onSave();
this.props.dispatch({ type: 'ADD_POST', response });
});
}
}
hideForm = () => { this.setState({ isFormVisible: false }); }
JSX Update
<Form
selectedData={this.state.selectedData}
isEdit={this.state.isEdit}
onSave={this.hideForm}
/>

componentWillReceiveProps is not triggered on react-router Link

So I have this component. It receives props and in the componentWillReceiveProps I set those props as state to fill in some form details. And it works when I manually type a url like this http://localhost:3000/dashboard/emailpreview/SKXj7t86agAzmRefG
It works great! However if I click on a react-router's Link that points to that url, the componentWillReceiveProps is not triggered at all, thus my form fields are not prefilled. But again if I perform a manual refresh there everything works. Why does this happen? What's the problem? Why componentWillReceiveProps doesn't trigger on Link?
import React, { Component } from 'react'
import { browserHistory } from 'react-router'
import { Editor } from 'react-draft-wysiwyg'
import { convertToRaw } from 'draft-js'
import { createContainer } from 'meteor/react-meteor-data'
import { Emails } from '../../../../../imports/collections/emails/Emails'
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'
import draftToHtml from 'draftjs-to-html'
import { stateFromHTML } from 'draft-js-import-html'
import _ from 'lodash'
class EmailEditor extends Component {
constructor (props) {
super(props)
this.state = {
name: '',
to: '',
subject: '',
html: '',
error: ''
}
}
// TODO: If there is id then fill in the information
// TODO: Subscribe and find and fill in with state
componentWillReceiveProps (nextProps) {
console.log('acsdcdsc', nextProps.email.name)
this.setState({name: nextProps.email.name, to: nextProps.email.to, subject: nextProps.email.subject})
}
handleChange (event) {
const changedOne = event.target.name
const newValue = event.target.value
const newState = {}
newState[changedOne] = newValue
this.setState(newState)
}
saveEmail () {
const self = this
console.log('saveEmail')
const { emailId } = this.props.params
console.log(emailId)
console.log('email')
const { name, to, subject, attachments, editorState } = this.state
console.log('editorState', editorState)
if (_.isEmpty(editorState)) {
self.setState({error: 'Please fill requaired fields'})
return
}
const rawContentState = convertToRaw(editorState.getCurrentContent())
const html = draftToHtml(rawContentState)
console.log('html', html)
const email = {
emailId,
name,
to,
subject,
html,
attachments // TODO: figure out how to send this
}
if (emailId === 'new') {
Meteor.call('emails.insert', email, (err, emailId) => {
if (err) {
self.setState({error: 'Please fill requaired fields'})
return
}
if (emailId) console.log(emailId)
browserHistory.push(`/dashboard/emailpreview/${emailId}`)
})
} else {
Meteor.call('emails.update', email, (err, emailId) => {
if (err) console.log(err)
if (emailId) console.log(emailId)
browserHistory.push(`/dashboard/emailpreview/${emailId}`)
})
}
}
renderEditor () {
return(
<div className="form-group">
<div><label style={{display: 'block', color: 'red', marginBottom: '10px'}}>*</label><input value={this.state.name} onChange={this.handleChange.bind(this)} type="text" name="name" placeholder="Email Name" /></div>
<div><label style={{display: 'block', color: 'red', marginBottom: '10px'}}>*</label><input value={this.state.to} onChange={this.handleChange.bind(this)} type="text" name="to" placeholder="To" /></div>
<div><label style={{display: 'block', color: 'red', marginBottom: '10px'}}>*</label><input value={this.state.subject} onChange={this.handleChange.bind(this)} type="text" name="subject" placeholder="Subject" /></div>
<div><span style={{display: 'block', color: 'red', margin: '10px'}}>{this.state.error}</span></div>
<Editor
toolbarClassName="wysiwig-toolbar"
wrapperClassName="wysiwig-wrapper"
editorClassName="wysiwig-editor"
onEditorStateChange={(editorState) => {
this.setState({
editorState
})
console.log(editorState)
}}
/>
<button onClick={this.saveEmail.bind(this)} className="btn btn-success">Save</button>
<button className="btn btn-primary">Send</button>
<button className="btn btn-primary">Test</button>
</div>
)
}
render () {
console.log('listItems1010', this.state)
console.log('listItems prop11010', this.props.email)
return (
<div className="EmailEditor">
{this.renderEditor()}
</div>
)
}
}
// https://jpuri.github.io/react-draft-wysiwyg/#/docs?_k=jjqinp
// {/* editorState={editorState}
// toolbarClassName="home-toolbar"
// wrapperClassName="home-wrapper"
// editorClassName="home-editor"
// onEditorStateChange={this.onEditorStateChange}
// uploadCallback={uploadImageCallBack} */}
export default createContainer((props) => {
const {emailId} = props.params
Meteor.subscribe('emails')
return {email: Emails.findOne(emailId)}
}, EmailEditor)
As it turned out componentWillReceiveProps is not triggered during initial rendering. It is only triggered on component update. So to setState on initial render I used componentDidMount for instance like this
componentDidMount () {
if (this.props.email) {
this.setState({
name: this.props.email.name,
to: this.props.email.to,
subject: this.props.email.subject,
html: this.props.email.html,
editorState: EditorState.createWithContent(stateFromHTML(this.props.email.html))
})
}
}
So the final code would look like
import React, { Component } from 'react'
import { browserHistory } from 'react-router'
import { Editor } from 'react-draft-wysiwyg'
import { convertToRaw, EditorState } from 'draft-js'
import { createContainer } from 'meteor/react-meteor-data'
import { Emails } from '../../../../../imports/collections/emails/Emails'
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'
import draftToHtml from 'draftjs-to-html'
import { stateFromHTML } from 'draft-js-import-html'
import _ from 'lodash'
class EmailEditor extends Component {
constructor (props) {
super(props)
this.state = {
name: '',
to: '',
subject: '',
html: '',
error: '',
editorState: EditorState.createEmpty()
}
}
componentWillReceiveProps (nextProps, nextContext) {
console.log('componentWillReceiveProps nextProps.email.name', nextProps.email.name)
this.setState({
name: nextProps.email.name,
to: nextProps.email.to,
subject: nextProps.email.subject,
html: nextProps.email.html,
editorState: EditorState.createWithContent(stateFromHTML(nextProps.email.html))
})
}
componentDidMount () {
if (this.props.email) {
this.setState({
name: this.props.email.name,
to: this.props.email.to,
subject: this.props.email.subject,
html: this.props.email.html,
editorState: EditorState.createWithContent(stateFromHTML(this.props.email.html))
})
}
}
handleChange (event) {
const changedOne = event.target.name
const newValue = event.target.value
const newState = {}
newState[changedOne] = newValue
this.setState(newState)
}
saveEmail () {
const self = this
console.log('saveEmail')
const { emailId } = this.props.params
console.log(emailId)
console.log('email')
const { name, to, subject, attachments, editorState } = this.state
console.log('editorState', editorState)
if (_.isEmpty(editorState)) {
self.setState({error: 'Please fill requaired fields'})
return
}
const rawContentState = convertToRaw(editorState.getCurrentContent())
const html = draftToHtml(rawContentState)
console.log('html', html)
const email = {
emailId,
name,
to,
subject,
html,
attachments // TODO: figure out how to send this
}
if (emailId === 'new') {
Meteor.call('emails.insert', email, (err, emailId) => {
if (err) {
self.setState({error: 'Please fill requaired fields'})
return
}
if (emailId) console.log(emailId)
browserHistory.push(`/dashboard/emailpreview/${emailId}`)
})
} else {
Meteor.call('emails.update', email, (err, result) => {
if (err) console.log(err)
if (result) console.log('update result', result)
browserHistory.push(`/dashboard/emailpreview/${emailId}`)
})
}
}
renderEditor () {
return (
<div className="form-group">
<div><label style={{display: 'block', color: 'red', marginBottom: '10px'}}>*</label><input
value={this.state.name}
onChange={this.handleChange.bind(this)}
type="text"
name="name"
placeholder="Email Name" /></div>
<div><label style={{display: 'block', color: 'red', marginBottom: '10px'}}>*</label><input
value={this.state.to}
onChange={this.handleChange.bind(this)}
type="text"
name="to"
placeholder="To" /></div>
<div><label style={{display: 'block', color: 'red', marginBottom: '10px'}}>*</label><input
value={this.state.subject}
onChange={this.handleChange.bind(this)}
type="text"
name="subject"
placeholder="Subject" /></div>
<div><span style={{display: 'block', color: 'red', margin: '10px'}}>{this.state.error}</span></div>
<Editor
editorState={this.state.editorState}
toolbarClassName="wysiwig-toolbar"
wrapperClassName="wysiwig-wrapper"
editorClassName="wysiwig-editor"
onEditorStateChange={(editorState) => {
this.setState({
editorState
})
console.log('editorState', editorState)
console.log('this.state', this.state)
}}
/>
<button onClick={this.saveEmail.bind(this)} className="btn btn-success">Save</button>
<button className="btn btn-primary">Send</button>
<button className="btn btn-primary">Test</button>
</div>
)
}
render () {
console.log('render state', this.state)
console.log('render props email', this.props.email)
return (
<div className="EmailEditor">
{this.renderEditor()}
</div>
)
}
}
// https://jpuri.github.io/react-draft-wysiwyg/#/docs?_k=jjqinp
// {/* editorState={editorState}
// toolbarClassName="home-toolbar"
// wrapperClassName="home-wrapper"
// editorClassName="home-editor"
// onEditorStateChange={this.onEditorStateChange}
// uploadCallback={uploadImageCallBack} */}
export default createContainer((props) => {
const {emailId} = props.params
Meteor.subscribe('emails')
return {email: Emails.findOne(emailId)}
}, EmailEditor)
The problem is componentWillReceiveProps is not called for the initial render. Read the documentation about the life cycle methods for a complete understanding http://reactjs.cn/react/docs/component-specs.html#lifecycle-methods
You can simply set the state in the constructor to fix the problem.
constructor (props) {
super(props)
this.state = {
name: props.name,
to: props.email.to,
subject: props.email.subject,
html: '',
error: ''
}
}
That code may generate some undefined errors though, a more complete solution would avoid the undefined errors with something similar to this:
constructor (props) {
super(props)
const { email, name } = props;
this.state = {
name,
to: email && email.to,
subject: email && email.subject,
html: '',
error: ''
}
}

Resources