Duplicates post but clears on hard refresh
I am creating an app which can display projects, this project has a few child components and server-side functions running when a user creates a new project. This results in the props changing which causes the ProjectList to refresh which is fine but it then also produces two of the submitted project when in the database it shows one and when you refresh it displays one.
Screenshot of duplicated project entry
As it works after refresh it makes me think its something to do with the state?
So I have a Dashboard component - > ProjectList -> ProjectListDetails
Dashboard code
import React, { Component } from "react";
import { connect } from "react-redux";
import { firestoreConnect } from "react-redux-firebase";
import ProjectList from "../Projects/ProjectList";
import { compose } from "redux";
import CreateProject from "../Projects/CreateProject";
import { Redirect } from "react-router-dom";
class Dashboard extends Component {
render() {
const { projects, auth, profile, organisations } = this.props;
if (!auth.uid) return <Redirect to="/signin" />;
//is the logged in user an Organisation or Single account?
const isOrg =
profile.isOrg === true ? (
<p>Organisation Account</p>
) : (
<p>Single Account</p>
);
return (
<div className="container">
<div className="row home-header-row">
<div className="col-md-12 section text-center">
{isOrg}
<br></br>
<CreateProject organisations={organisations} />
<ProjectList
projects={projects}
authID={auth.uid}
profile={profile}
/>
</div>
</div>
</div>
);
}
}
const mapStateToProps = state => {
return {
projects: state.firestore.ordered.projects,
auth: state.firebase.auth,
profile: state.firebase.profile,
notifications: state.firestore.ordered.notifications,
organisations: state.firestore.ordered.organisations
};
};
export default compose(
connect(mapStateToProps),
firestoreConnect([
{ collection: "projects", orderBy: ["totalEntries", "desc"] },
{ collection: "organisations", orderBy: ["time", "desc"] },
{ collection: "notifications", limit: 5, orderBy: ["time", "desc"] }
])
)(Dashboard);
Project List Component :
import React, { Component } from "react";
import ProjectListDetails from "./ProjectListDetails";
import "firebase/firestore";
class ProjectList extends Component {
// shouldComponentUpdate(nextProps, nextState) {
// return this.props.projects !== nextProps.projects;
// }
render() {
return (
<div className="project-list section">
{this.props.projects &&
this.props.projects.map(project => {
const projectId =
project.authorID +
project.createdAt.toDate().toLocaleTimeString("it-it") +
project.title;
return (
<>
{project.canView.includes(this.props.profile.email) ||
project.authorEmail.includes(this.props.profile.email) ? (
<ProjectListDetails
project={project}
profile={this.props.profile}
projectId={projectId}
></ProjectListDetails>
) : null}
</>
);
})}
</div>
);
}
}
export default ProjectList;
ProjectListDetails Component
import React, { Component } from "react";
import EditProject from "./EditProjects";
import DeleteProject from "./DeleteProject";
import { Link } from "react-router-dom";
import ProjectSummary from "./ProjectSummary";
class ProjectListDetails extends Component {
render() {
console.log("printing project : " + this.props.project.title);
return (
<div className="row" key={this.props.project.id}>
<div className="col-md-12" key={this.props.project.projectID}>
<button
className="btn btn-secondary dropdown-toggle project-options"
type="button"
id="dropdownMenuButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
Options
</button>
<div
className="dropdown-menu"
aria-labelledby="dropdownMenuButton"
id={this.props.project.projectID}
>
<EditProject />
<DeleteProject />
</div>
<Link
to={"/project/" + this.props.project.id}
key={this.props.project.id}
profile={this.props.profile}
projectId={this.props.projectId}
>
<ProjectSummary
projectId={this.props.projectId}
project={this.props.project}
key={this.props.project.id}
authID={this.props.authID}
/>
</Link>
</div>
</div>
);
}
}
export default ProjectListDetails;
CreateProject Component
import React, { Component } from "react";
import { connect } from "react-redux";
import { createProject } from "../../Store/Actions/projectActions";
import Button from "react-bootstrap/Button";
import Modal from "react-bootstrap/Modal";
// import ReactQuill from 'react-quill'
// import 'react-quill/dist/quill.snow.css'
// import renderHTML from 'react-render-html';
class CreateProject extends Component {
state = {
title: "",
content: "",
projectAmount: 0,
commentAmount: 0,
hideModal: false,
showModal: "",
show: false,
setShow: false,
canView: [],
organisation: ""
};
handleChange = e => {
this.setState({
[e.target.id]: e.target.value
});
};
handleSubmit = e => {
e.preventDefault();
this.setState({ canView: [] });
this.props.organisations &&
this.props.organisations.map(organisation => {
if (organisation.canView.includes(this.props.profile.email)) {
const orgUser = organisation.canView.map((item, key) =>
this.state.canView.push(item)
);
return orgUser;
}
return null;
});
this.props.createProject(this.state);
document.getElementById("title").value = "";
document.getElementById("content").value = "";
};
handleClose = () => {
this.setState({
show: false
});
};
handleShow = () => {
this.setState({
show: true
});
};
render() {
return (
<>
<Button
className="btn btn-primary"
variant="primary"
onClick={this.handleShow}
>
New Project
</Button>
<Modal show={this.state.show} onHide={this.handleClose}>
<Modal.Header closeButton>
<Modal.Title>New Project</Modal.Title>
</Modal.Header>
<Modal.Body>
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<input
onChange={this.handleChange}
className="form-control"
type="text"
id="title"
placeholder="Project Title"
/>
</div>
<div className="form-group">
<textarea
onChange={this.handleChange}
className="form-control"
id="content"
rows="3"
placeholder="Project Content (can add some more options here later)"
></textarea>
</div>
<Modal.Footer>
<Button variant="secondary" onClick={this.handleClose}>
Close
</Button>
<Button
type="submit"
variant="primary"
onClick={this.handleClose}
>
Save Changes
</Button>
</Modal.Footer>
</form>
</Modal.Body>
</Modal>
</>
);
}
}
const mapStateToProps = state => {
return {
projects: state.firestore.ordered.projects,
auth: state.firebase.auth,
profile: state.firebase.profile,
organisations: state.firestore.ordered.organisations
};
};
const mapDispatchToProps = dispatch => {
return {
createProject: project => dispatch(createProject(project))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(CreateProject);
Project Action
export const createProject = project => {
return (dispatch, getState, { getFirebase, getFirestore }) => {
// make async call to database
const firestore = getFirestore();
const profile = getState().firebase.profile;
const authorId = getState().firebase.auth.uid;
const createdAt = new Date();
const convertedDate = createdAt.toLocaleTimeString("it-it");
const printFirstName =
profile.isOrg === true ? profile.orgFirstName : profile.firstName;
const printLastName =
profile.isOrg === true ? profile.orgLastName : profile.lastName;
const projectId = authorId + convertedDate + project.title;
const orgName = profile.organisation ? profile.organisation : "";
//this.state.project.createdAt.toDate().toLocaleTimeString('it-it') + this.state.project.title
firestore
.collection("projects")
.doc(projectId)
.set({
...project,
authorFirstName: printFirstName,
authorLastname: printLastName,
authorID: authorId,
authorEmail: profile.email,
canView: project.canView,
canEdit: [profile.email],
createdAt: createdAt,
projectID: projectId,
organisation: orgName,
permalink: "/project/" + projectId
})
.then(() => {
dispatch({ type: "CREATE_PROJECT", project });
})
.catch(err => {
dispatch({ type: "CREATE_PROJECT_ERROR", err });
});
//
};
};
ProjectReducer
const initState = {};
const projectReducer = (state = initState, action) => {
switch (action.type) {
case "CREATE_PROJECT":
return state;
case "DELETE_PROJECT":
console.log("Project Deleted", action.project);
return state;
case "CREATE_PROJECT_ERROR":
console.log("Create project error", action.err);
return state;
case "DELETE_PROJECT_ERROR":
console.log("Delete project error", action.err);
return state;
default:
return state;
}
};
export default projectReducer;
Firebase Function
exports.projectInnerEntries = functions.firestore
.document("projects/{projectId}")
.onWrite((change, context) => {
const newValue = change.after.data();
var projectAmount = newValue.projectAmount;
var commentAmount = newValue.commentAmount;
// var totalEntries = projectAmount + commentAmount;
const notification = {
projects: projectAmount,
comments: commentAmount,
total: projectAmount + commentAmount
};
return admin
.firestore()
.collection("projects")
.doc(newValue.projectID)
.update({
totalEntries: notification.total
});
});
Same here when updating object or setting a new one, any news about it?
@AlexHyslop is the data ever making it to the database? Could you also post your security rules so I can be sure I'm replicating fully
Something else to keep in mind is that your createProject method is actually async, and you are manually clearing out the fields without confirming that the project is actually written. I see that you are using action dispatches to handle the error case - do you happen to see that action dispatched? Part of the goal of this library was to prevent the need for making a set of actions every time an app is writing to the DB (since this causes tons of unnecessary duplication in larger applications)
I'm having the same issue without custom reducer. I use the following to overwrite my items (e.g. project):
return (storeID, id, item) => {
firestore
.doc(`${generateCollectionPath(auth.uid, storeID)}/${id}`)
.set(item);
};
and then on the screen before I use the following to read it:
const auth = useSelector((state) => state.firebase.auth);
const storeName = `store-${storeID}`;
useFirestoreConnect([
{
collection: generateCollectionPath(auth.uid, storeID),
storeAs: storeName,
},
]);
return useSelector((state) => state.firestore.ordered[storeName]);
When I print out everything to the console I see the duplicated entry (old and new entry):
console.log(JSON.stringify(revisionItems, null, 2));
As far as I see the duplicates never make it to firestore.