frontend-collective / react-sortable-tree

Drag-and-drop sortable component for nested data and hierarchies
https://frontend-collective.github.io/react-sortable-tree/
MIT License
4.91k stars 904 forks source link

[when using hooks] Cannot update a component from inside the function body of a different component. #687

Open radulle opened 4 years ago

radulle commented 4 years ago

When storyboard class component search example is converted to functional (hooks) errors/warnings are thrown.

import React, { Component } from "react"
import SortableTree from "react-sortable-tree"
import "react-sortable-tree/style.css"

const data = [
  {
    title: "Windows 10",
    subtitle: "running",
    children: [
      {
        title: "Ubuntu 12",
        subtitle: "halted",
        children: [
          {
            title: "Debian",
            subtitle: "gone"
          }
        ]
      },
      {
        title: "Centos 8",
        subtitle: "hardening"
      },
      {
        title: "Suse",
        subtitle: "license"
      }
    ]
  }
]

const nodeInfo = row => console.log(row)

export default class App extends Component {
  constructor(props) {
    super(props)

    this.state = {
      searchString: "",
      searchFocusIndex: 0,
      searchFoundCount: null,
      treeData: data
    }
  }

  render() {
    const { searchString, searchFocusIndex, searchFoundCount } = this.state

    const customSearchMethod = ({ node, searchQuery }) =>
      searchQuery &&
      ((node.title &&
        node.title.toLowerCase().indexOf(searchQuery.toLowerCase()) > -1) ||
        (node.subtitle &&
          node.subtitle.toLowerCase().indexOf(searchQuery.toLowerCase()) > -1))

    const selectPrevMatch = () =>
      this.setState({
        searchFocusIndex:
          searchFocusIndex !== null
            ? (searchFoundCount + searchFocusIndex - 1) % searchFoundCount
            : searchFoundCount - 1
      })

    const selectNextMatch = () =>
      this.setState({
        searchFocusIndex:
          searchFocusIndex !== null
            ? (searchFocusIndex + 1) % searchFoundCount
            : 0
      })

    return (
      <div>
        <h2>Find the needle!</h2>
        <form
          style={{ display: "inline-block" }}
          onSubmit={event => {
            event.preventDefault()
          }}
        >
          <input
            id="find-box"
            type="text"
            placeholder="Search..."
            style={{ fontSize: "1rem" }}
            value={searchString}
            onChange={event =>
              this.setState({ searchString: event.target.value })
            }
          />
          &nbsp;
          <button
            type="button"
            disabled={!searchFoundCount}
            onClick={selectPrevMatch}
          >
            &lt;
          </button>
          &nbsp;
          <button
            type="submit"
            disabled={!searchFoundCount}
            onClick={selectNextMatch}
          >
            &gt;
          </button>
          &nbsp;
          <span>
            &nbsp;
            {searchFoundCount > 0 ? searchFocusIndex + 1 : 0}
            &nbsp;/&nbsp;
            {searchFoundCount || 0}
          </span>
        </form>

        <div style={{ height: 300 }}>
          <SortableTree
            treeData={this.state.treeData}
            onChange={treeData => this.setState({ treeData })}
            searchMethod={customSearchMethod}
            searchQuery={searchString}
            searchFocusOffset={searchFocusIndex}
            searchFinishCallback={matches =>
              this.setState({
                searchFoundCount: matches.length,
                searchFocusIndex:
                  matches.length > 0 ? searchFocusIndex % matches.length : 0
              })
            }
            generateNodeProps={row => {
              return {
                title: row.node.title,
                subtitle: (
                  <div style={{ lineHeight: "2em" }}>{row.node.subtitle}</div>
                ),
                buttons: [
                  <button
                    type="button"
                    className="btn btn-outline-success"
                    style={{
                      verticalAlign: "middle"
                    }}
                    onClick={() => nodeInfo(row)}
                  >
                    ℹ
                  </button>
                ]
              }
            }}
          />
        </div>
      </div>
    )
  }
}

image

NickEmpetvee commented 4 years ago

@radulle did you ever get this working with hooks? I haven't tried to convert my implementation yet.

pjmvp commented 4 years ago

Any news on this one? The error message is actually just a warning and everything seems to work. But it might break in future versions of React.

This is related to the following React issue: https://github.com/facebook/react/issues/18147

Ref. comment: https://github.com/facebook/react/issues/18147#issuecomment-592267650

The problem is that the proposed solution would require components in react-sortable-tree to be converted from class components to functional ones...

IDrissAitHafid commented 4 years ago

I have the same issue!

jinmingpang commented 4 years ago

Any news on this one? I have the same problem~

IDrissAitHafid commented 4 years ago

after going through this issue and reading this, I understood that the warning gets triggered if you want to setState a component synchronously from a different component.

So I have found a workaround to make the warning disappear, till it's fixed.

If you click on the little arrow beside Warning, you'll get a much detailed stacktrace. For me, I have found the warning is triggered by the callback I pass to searchFinishCallback (because it's there where I change the state of my component from SortableTree component).

So, I changed my callback from something like this:

const searchFinishCallback = (matches) => {
      setSearchFoundCount(matches.length)
      setSearchFocusIndex((searchFocusIndex) =>
        matches.length > 0 ? searchFocusIndex % matches.length : 0
      )
  } 

To something like this:

const searchFinishCallback = (matches) => {
    setImmediate(() => {
      setSearchFoundCount(matches.length)
      setSearchFocusIndex((searchFocusIndex) =>
        matches.length > 0 ? searchFocusIndex % matches.length : 0
      )
    })
  } 

You can instead use setTimeout or anything that will make the call async.