Search code examples
reactjsreact-reduxreact-dnd

With react DnD, why my state resets when every time I drop an element?


I am working on a form builder. The basic implementation works but every time when I move elements from the tool box to the form the current state resets to initial then add newly dropped element so previously dragged element is missed from the form. Below is the linked to the source code at sandbox.

https://codesandbox.io/s/nifty-https-qx8lko

index.js

import Example from "./Example.jsx";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";

import "./style.sass";

function App() {
  return (
    <div className="App">
      <DndProvider backend={HTML5Backend}>
        <Example />
      </DndProvider>
    </div>
  );
}

const rootElement = document.getElementById("root");
render(<App />, rootElement);

Example.jsx

import React, { useState } from "react";
import Form from "./Form";
import Toolbar from "./Toolbar";

const Example = (props) => {
  const [Items, setItems] = useState([{ name: "Test" }]);

  const addItem = (item) => {
    setItems([...Items, item]);
  };

  return (
    <div className="card" style={{ border: "1px dashed rgb(219 163 163)" }}>
      <div className="card-header  d-flex justify-content-between">
        <h5 className="cart-title m-0">View</h5>
      </div>
      <div className="card-body">
        <div className="row">
          <div className="col-9">
            <Form items={Items} addItems={addItem} />
          </div>
          <div className="col-3">
            <Toolbar />
          </div>
        </div>
      </div>
    </div>
  );
};

export default Example;

Form.jsx

import React from "react";
import { useDrop } from "react-dnd";

import { ItemTypes } from "./ItemTypes";

function Form(props) {
  const addItem = (item) => {
    props.addItems(item);
  };

  const [{ canDrop, isOver }, drop] = useDrop(() => ({
    accept: ItemTypes.CARD,
    drop: (item, monitor) => addItem(item),
    collect: (monitor) => ({
      isOver: monitor.isOver(),
      canDrop: monitor.canDrop()
    })
  }));

  return (
    <div className="card card-default shadow-sm mt-3">
      <div className="card-body">
        {props.items.map((item, index) => {
          return (
            <div key={index} className="form-item">
              {item.name}
            </div>
          );
        })}
        <div ref={drop}>
          {/* {canDrop ? "Release to drop" : "Drag a box here"} */}
          {isOver && canDrop && (
            <div className="form-place-holder">
              <div>Release to Drop</div>
            </div>
          )}
          {!isOver && (
            <div className="form-place-holder">
              <div>Dropzone</div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

export default Form;

Toolbar.jsx

import React from "react";

import ToolbarItem from "./ToolbarItem";

const Toolbar = () => {
  const _defaultItems = () => {
    return [
      {
        key: "Header",
        name: "Header Text",
        icon: "fas fa-heading",
        static: true,
        content: "Place holder text"
      },
      {
        key: "Label",
        name: "Label",
        static: true,
        icon: "fas fa-font",
        content: "Place holder text"
      },
      {
        key: "Paragraph",
        name: "Paragraph",
        static: true,
        icon: "fas fa-paragraph",
        content: "Place holder text"
      },
      {
        key: "LineBreak",
        name: "Line break",
        static: true,
        icon: "fas fa-arrows-alt-h"
      },
      {
        key: "Dropdown",
        canHaveAnswer: true,
        name: "Dropdown",
        icon: "far fa-caret-square-down",
        label: "Place holder label",
        field_name: "dropdown_",
        options: []
      },
      {
        key: "Tags",
        canHaveAnswer: true,
        name: "Tags",
        icon: "fas fa-tags",
        label: "Place holder label",
        field_name: "tags_",
        options: []
      },
      {
        key: "Checkboxes",
        canHaveAnswer: true,
        name: "Checkboxes",
        icon: "far fa-check-square",
        label: "Place holder label",
        field_name: "checkboxes_",
        options: []
      },
      {
        key: "RadioButtons",
        canHaveAnswer: true,
        name: "Multiple choices",
        icon: "far fa-dot-circle",
        label: "Place holder label",
        field_name: "radiobuttons_",
        options: []
      },
      {
        key: "TextInput",
        canHaveAnswer: true,
        name: "Text input",
        label: "Place holder label",
        icon: "fas fa-font",
        field_name: "text_input_"
      },
      {
        key: "NumberInput",
        canHaveAnswer: true,
        name: "Number input",
        label: "Place holder label",
        icon: "fas fa-plus",
        field_name: "number_input_"
      },
      {
        key: "TextArea",
        canHaveAnswer: true,
        name: "Multi line input",
        label: "Place holder label",
        icon: "fas fa-text-height",
        field_name: "text_area_"
      },
      {
        key: "TwoColumnRow",
        canHaveAnswer: false,
        name: "Two columns row",
        label: "",
        icon: "fas fa-columns",
        field_name: "two_col_row_"
      },
      {
        key: "ThreeColumnRow",
        canHaveAnswer: false,
        name: "Three columns row",
        label: "",
        icon: "fas fa-columns",
        field_name: "three_col_row_"
      },
      {
        key: "FourColumnRow",
        canHaveAnswer: false,
        name: "Four columns row",
        label: "",
        icon: "fas fa-columns",
        field_name: "four_col_row_"
      },
      {
        key: "Image",
        name: "Image",
        label: "",
        icon: "far fa-image",
        field_name: "image_",
        src: ""
      },
      {
        key: "Rating",
        canHaveAnswer: true,
        name: "Rating",
        label: "Place holder label",
        icon: "fas fa-star",
        field_name: "rating_"
      },
      {
        key: "DatePicker",
        canDefaultToday: true,
        canReadOnly: true,
        dateFormat: "MM/dd/yyyy",
        timeFormat: "hh:mm aa",
        showTimeSelect: false,
        showTimeSelectOnly: false,
        showTimeInput: false,
        name: "Date",
        icon: "far fa-calendar-alt",
        label: "Place holder label",
        field_name: "date_picker_"
      },
      {
        key: "Signature",
        canReadOnly: true,
        name: "Signature",
        icon: "fas fa-pen-square",
        label: "Signature",
        field_name: "signature_"
      },
      {
        key: "HyperLink",
        name: "Website",
        icon: "fas fa-link",
        static: true,
        content: "Place holder website link",
        href: "http://www.example.com"
      },
      {
        key: "Download",
        name: "File attachment",
        icon: "fas fa-file",
        static: true,
        content: "Place holder file name",
        field_name: "download_",
        file_path: "",
        _href: ""
      },
      {
        key: "Range",
        name: "Range",
        icon: "fas fa-sliders-h",
        label: "Place holder label",
        field_name: "range_",
        step: 1,
        default_value: 3,
        min_value: 1,
        max_value: 5,
        min_label: "Easy",
        max_label: "Difficult"
      },
      {
        key: "Camera",
        name: "Camera",
        icon: "fas fa-camera",
        label: "Place holder label",
        field_name: "camera_"
      },
      {
        key: "FileUpload",
        name: "File upload",
        icon: "fas fa-file",
        label: "Place holder label",
        field_name: "file_upload_"
      }
    ];
  };
  return (
    <div className="react-form-builder-toolbar">
      <h4>Toolbox</h4>
      <ul>
        {_defaultItems().map((item, index) => {
          return <ToolbarItem data={item} key={index} />;
        })}
      </ul>
    </div>
  );
};

export default Toolbar;

ToolbarItem.jsx

import React from "react";
import { useDrag } from "react-dnd";

import { ItemTypes } from "./ItemTypes";

function ToolbarItem(props) {
  const data = props.data;
  const [{ isDragging }, drag, dragPreview] = useDrag(() => ({
    type: ItemTypes.CARD,
    collect: (monitor) => ({
      isDragging: monitor.isDragging()
    }),
    item: props.data
  }));
  return (
    <li ref={drag} style={{ opacity: isDragging ? 0.5 : 1 }}>
      <i className={data.icon}></i>
      {data.name}
    </li>
  );
}

export default ToolbarItem;

I have tried also storing the state in redux and still same behavior. For simplicity I have kept the state in parent component in Sandbox example.


Solution

  • In Example.js file try replacing with this setItems((items)=>[...items, item]);

    I think this will fix the issue. Check this here https://codesandbox.io/s/reactjs-dnd-example-forked-1lu5i9?file=/src/Form.jsx