ReactJS - Page re-renders but one component stays the same. (Beautiful Drag&Drop)

985 views Asked by At

I'm having a very weird issue where just one component on a page is not being refreshed, and I just can't figure out why.

Here's a short video of the problem:

https://i.gyazo.com/45e229b0867c37e48a18da7a55afb522.mp4

Notice how the question string changes when I click confirm (as it should), but the cards of the drag and drop window stay the same. It keeps displaying the question name "yooo" and the answers "abc, def", while that's only valid for the first question.

I'm still relatively new to ReactJS, so there may be some functionality here that I'm not familiar with? As far as I know DragAndDrop should be re-rendered entirely with the next question. Currently the constructor is not being called again and it saves the data of the last question somehow.

Render of this page. DragAndDrop is being called here. In confirm(), currentQuestion is being set to the next question.

return (
    <div>
      <h3>{currentQuestion.question}</h3>

      <DragAndDrop
        answers={currentQuestion.answers}
      />

      <Button onClick={() => confirm()}>Confirm</Button>
    </div>
  );

Entire DragAndDrop.js Sorry about the wall of code, it's almost the same as the example code from Beautiful-DND https://codesandbox.io/s/k260nyxq9v

/* eslint-disable no-console */
/* eslint-disable react/prop-types */
import React, { Component } from "react";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";

// STYLING
const grid = 8;

const getItemStyle = (isDragging, draggableStyle) => ({
  // some basic styles to make the items look a bit nicer
  userSelect: "none",
  padding: grid * 2,
  margin: `0 0 ${grid}px 0`,

  // change background colour if dragging
  background: isDragging ? "cyan" : "white",

  // styles we need to apply on draggables
  ...draggableStyle,
});

const getListStyle = (isDraggingOver) => ({
  background: isDraggingOver ? "lightblue" : "lightgrey",
  padding: grid,
  width: "100%",
});

// a little function to help us with reordering the result
const reorder = (list, startIndex, endIndex) => {
  const result = Array.from(list);
  const [removed] = result.splice(startIndex, 1);
  result.splice(endIndex, 0, removed);

  return result;
};

export default class DragAndDrop extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: props.answers,
    };
    this.onDragEnd = this.onDragEnd.bind(this);

    console.log("Answers & items");
    console.log(this.props.answers);
    console.log(this.state.items);
  }

  onDragEnd(result) {
    // dropped outside list
    if (!result.destination) {
      return;
    }

    const items = reorder(
      this.state.items,
      result.source.index,
      result.destination.index
    );

    this.setState({
      items,
    });
  }
  render() {
    return (
      <DragDropContext onDragEnd={this.onDragEnd}>
        <Droppable droppableId="droppable">
          {(provided, snapshot) => (
            <div
              {...provided.droppableProps}
              ref={provided.innerRef}
              style={getListStyle(snapshot.isDraggingOver)}
            >
              {this.state.items.map((item, index) => (
                <Draggable
                  key={item.id}
                  draggableId={item.id.toString()}
                  index={index}
                >
                  {(provided, snapshot) => (
                    <div
                      ref={provided.innerRef}
                      {...provided.draggableProps}
                      {...provided.dragHandleProps}
                      style={getItemStyle(
                        snapshot.isDragging,
                        provided.draggableProps.style
                      )}
                    >
                      {
                        item.answer +
                          " index: " +
                          index +
                          " ordering:" +
                          item.ordering /*CONTENT OF CARD*/
                      }
                    </div>
                  )}
                </Draggable>
              ))}
              {provided.placeholder}
            </div>
          )}
        </Droppable>
      </DragDropContext>
    );
  }
}
2

There are 2 answers

11
Osama Sayed On BEST ANSWER

I think the problem lies in this line in the constructor:

this.state = {
  items: props.answers,
};

Setting items like this in the constructor means you are ignoring any following updates to props from the parent component! If you check the official documentation, they warn against this.

Avoid copying props into state! This is a common mistake:

The problem is that it’s both unnecessary (you can use this.props.color directly instead), and creates bugs (updates to the color prop won’t be reflected in the state).

Only use this pattern if you intentionally want to ignore prop updates. In that case, it makes sense to rename the prop to be called initialColor or defaultColor. You can then force a component to “reset” its internal state by changing its key when necessary.

If you want to depend on the values of the props and change the state accordingly, you can use static getDerivedStateFromProps()

Here is a working example using a class-based component which is just a proof-of-concept using your component with static getDerivedStateFromProps() (which is not deprecated!). I added some dummy data that uses the same structure you provided in the parent component which changes when you click "Confirm". Also, here is a working example using hooks doing the same thing which uses useState and useEffect hooks.

3
breakit On

props.answers is controlled by your parent component and confirm function.Your DragAndDrop component only sets props to state initially in constructor. It doesn't know when props getting changed as only for first time you set props tostate in constructor. You can simulate props change in multiple ways as below :

  1. Simulating state change with props change
...
constructor(props) {
    this.state = {items:props.answers}
...
}
componentDidUpdate(prevProps, PrevState) {
    if (this.props.answers && this.props.answers !== prevProps.answers) {
      this.setState({ items: this.props.answers });
    }
  }
  1. Using props directly, no state in constructor or anywhere in your DragAndDrop component

  2. Moving your props andconfirm directly in your DragAndDrop component