0

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>
    );
  }
}
SJ19
  • 1,933
  • 6
  • 35
  • 68
  • Can you share the content of `this.props.answers`? – Edgar Henriquez Sep 26 '20 at 17:11
  • @EdgarHenriquez https://i.gyazo.com/04226d92f02d2fbbfd49626e7d36772b.png It only prints the answers of the first question (abc, def), the next two questions have different answers but they don't load. – SJ19 Sep 26 '20 at 20:04
  • It seems like your problem is in the Parent component then, I see no problems with your code, it's being passed 2 questions as props (according to your screenshot) and it's generating 2 draggable items with the right `ordering` and `answer`. – Edgar Henriquez Sep 27 '20 at 00:58
  • @EdgarHenriquez I don't see why the problem would be in the parent component? In the parent component the currentQuestion.question and currentQuestion.answers are correct (and they change when I click confirm), there are 3 different questions with different answers. Even though the question changes on the page, it doesn't change in the DragAndDrop. You should stop looking at the ordering and answer, the functionality of the Drag And Drop are fine, the problem is they don't reload on the next question. The constructor is not called at all on re-render of the page. – SJ19 Sep 27 '20 at 11:16

2 Answers2

1

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.

Osama Sayed
  • 1,993
  • 15
  • 15
  • Could you maybe show an example where you use getDerivedStateFromProps in my case? Also, React documentation says it's legacy and shouldn't be used.. – SJ19 Sep 29 '20 at 09:32
  • @SJ19 getDerivedStateFromProps I edited my answer and provided a working example with getDerivedStateFromProps. Please have a look. – Osama Sayed Sep 29 '20 at 10:42
  • @SJ19 also, React doesn't say it shouldn't be used neither does it say it's legacy. It just says "This method exists for rare use cases where the state depends on changes in props over time" and also says "Deriving state leads to verbose code and makes your components difficult to think about.". – Osama Sayed Sep 29 '20 at 10:45
  • My bad, I looked wrongly, it is indeed not depecrated. :) – SJ19 Sep 29 '20 at 11:12
  • 1
    Absolute legend. It works perfectly now, thank you so much! – SJ19 Sep 29 '20 at 11:13
  • It's weird that I've never encountered this issue before though, I'm pretty sure I always call components this way (without derivedfromprops). – SJ19 Sep 29 '20 at 11:14
  • No worries. Glad it's working!! :) yes, typically needing to use getDerivedStateFromProps is very rare but in your case where you depend on the props first to populate the value of the state, which is then changed internally is a valid case for using it. – Osama Sayed Sep 29 '20 at 11:23
  • Thanks again for your help :) Remind me to award your bounty. I need to wait 20 hours. – SJ19 Sep 29 '20 at 11:47
  • Btw, I hope this isn't too much to ask. How would you do this if DragAndDrop was a function instead of a class? Or does it have to be a class? I'm asking this because I usually always use functions for components. – SJ19 Sep 29 '20 at 11:48
  • @SJ19 I have edited my answer to include a working example using hooks. You can check it out! :) – Osama Sayed Oct 01 '20 at 06:33
  • You're awesome :) I'll check it out. I have another problem now, I would like to call a function which shuffles the answers, however it appears that I cannot call any functions in getDerivedStateFromProps? Where am I supposed to put it? – SJ19 Oct 01 '20 at 16:13
1

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

breakit
  • 356
  • 2
  • 8