tajo / react-movable

🔀 Drag and drop for your React lists and tables. Accessible. Tiny.
https://react-movable.pages.dev
MIT License
1.53k stars 51 forks source link

React useState doesn't update correctly #99

Closed cstrat closed 2 months ago

cstrat commented 10 months ago

Unsure if I am doing something wrong here, but I have an issue where my state isn't being updated in the onChange hook.

I get a weird effect where after dragging and dropping, the list looks like it did prior to the drag & drop. If I try to drag the list item from where I left it, it transforms into the old element.

2023-11-06 15 00 11

See the example above.

I tried searching for a similar issue in the issues here, and noticed someone else implemented a setTimeout which actually fixed it, but then created a flash of missing content in my modal which is not desirable...

Am I doing something wrong here?

cstrat commented 10 months ago

This is the code I am using. Using Mantine library for UI.

I originally had the data in a parent component and passed the state and state updater to this one, but thought that was my issue. So now I am saving local state and not even worrying about the parent state.

function LicenseEditor({ showLicence, licenseData, user }) {
  const [localLD, setLocalLD] = useState(licenseData);

  return (
    <Fieldset mb="md" legend="License Editor">
      <List
        values={localLD}
        onChange={({ oldIndex, newIndex }) => setLocalLD(arrayMove(localLD, oldIndex, newIndex))}
        renderList={({ children, props, isDragged }) => (
          <Table
            striped
            highlightOnHover
            withColumnBorders
            style={{
              fontSize: `${user.preferences?.adminTableSize || 0.85}rem`,
              cursor: isDragged ? "grabbing" : undefined,
            }}
          >
            <Table.Thead>
              <Table.Tr>
                <Table.Th style={{ width: "30%" }}>SKU</Table.Th>
                <Table.Th style={{ width: "35%" }}>Name</Table.Th>
                <Table.Th style={{ width: "20%" }}>Term</Table.Th>
                <Table.Th style={{ width: "15%" }}></Table.Th>
              </Table.Tr>
            </Table.Thead>
            <Table.Tbody>
              <Table.Tr>
                <Table.Td>
                  <TextInput
                    name="sku"
                    placeholder="License SKU"
                  />
                </Table.Td>
                <Table.Td>
                  <Autocomplete
                    name="name"
                    placeholder="Friendly Name"
                  />
                </Table.Td>
                <Table.Td>
                  <NumberInput
                    step={12}
                    name="term"
                    placeholder="Term in Months"
                  />
                </Table.Td>
                <Table.Td>
                  <Button fullWidth size="compact-sm" onClick={addNewLicence}>
                    Add
                  </Button>
                </Table.Td>
              </Table.Tr>
            </Table.Tbody>
            <Table.Tbody {...props}>{children}</Table.Tbody>
          </Table>
        )}
        renderItem={({ value, props, isDragged, isSelected }) => {
          const row = (
            <Table.Tr key={value.id} {...props} style={{ cursor: isDragged ? "grabbing" : "grab" }}>
              <Table.Td>
                <input type="hidden" name="licenseID" value={value.id} />
                <TextInput name="licenseSKU" defaultValue={value.sku} placeholder="License SKU" />
              </Table.Td>
              <Table.Td>
                <Autocomplete
                  name="licenseName"
                  defaultValue={value.name}
                  placeholder="Friendly Name"
                  data={[...localLD.map((lic) => lic.name).filter((v, i, a) => a.indexOf(v) === i)]}
                />
              </Table.Td>
              <Table.Td>
                <NumberInput name="licenseTerm" defaultValue={value.term} step={12} placeholder="Term in Months" />
              </Table.Td>
              <Table.Td>
                <Group grow>
                  <Button variant="light" color="blue" size="compact-sm" data-movable-handle>
                    <IconGripHorizontal size="1.2rem" />
                  </Button>
                  <Button variant="light" color="blue" size="compact-sm">
                    <IconTrash size="1.2rem" />
                  </Button>
                </Group>
              </Table.Td>
            </Table.Tr>
          );

          return isDragged ? (
            <Table style={{ ...props.style, borderSpacing: 0, zIndex: 205 }}>
              <Table.Tbody>{row}</Table.Tbody>
            </Table>
          ) : (
            row
          );
        }}
      />
    </Fieldset>
  );
}
cstrat commented 10 months ago

I also tried stripping this back to just regular <table> - even though that wouldn't make sense to fix things, just because I was pulling my hair out... and it still happens.

MoltenCoffee commented 9 months ago

I had a similar issue which I've struggled with for a few hours. Not entirely sure if it's the same, and also not entirely sure whether it was a bug in the library, or in the way React handles arrays/children, or the way browsers keep track of input elements.

In my case, I had a form, to which the user can add new input elements (textareas) and move them around. I was using controlled components. On creation, value would be left empty, and an onChange would store the value in component state when the user changed it contents for the first time, and of course thereafter. When I would add a new (and thus, empty) textarea to the form, and move it around, it would copy the value of a different field, but no onChange event would be fired and no state stored.

In my case, the solution was to store the value of a newly created textarea as an empty string in state.

Seeing as your example used defaultValue, I'd assume something similar is/was happening to your code.

Edit: React 18.2.0, in both Chrome 120 and Firefox 114

tajo commented 2 months ago

You have to keep the state of dragged components outside of those components.