react-redux-firebase icon indicating copy to clipboard operation
react-redux-firebase copied to clipboard

Duplicates post but clears on hard refresh

Open AlexHyslop opened this issue 6 years ago • 3 comments

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

After page refresh

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
      });
  });

AlexHyslop avatar Dec 13 '19 08:12 AlexHyslop

Same here when updating object or setting a new one, any news about it?

ViniciusPaldes avatar Dec 10 '20 22:12 ViniciusPaldes

@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)

prescottprue avatar Dec 28 '20 20:12 prescottprue

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.

a-zen avatar Apr 23 '21 11:04 a-zen