未处理的拒绝 (TypeError):result.push 不是函数

Unhandled Rejection (TypeError): result.push is not a function

这是我的反应组件中的代码

const mockFetch = () => Promise.resolve({
  json: () => new Promise((resolve) => setTimeout(() => resolve({
    student1: {
      studentName: 'student1'
    },
    student2: {
      studentName: 'student2'
    },
    student3: {
      studentName: 'student3'
    }
  }), 250))
})

function MyApp() {
  const [result, setResult] = React.useState([]);
  const [queryString, setQueryString] = React.useState("");

  function searchHandler() {
    mockFetch(
        "{url that i defined in real code}"
      )
      .then(response => response.json())
      .then(data => {
        Object.keys(data).filter(element => {
          if (queryString === "") {
            setResult(result.push(data[element].studentName));
          } else if (data[element].studentName.toLowerCase().includes(queryString.toLowerCase())) {
            setResult(result.push(data[element].studentName));
          }
          setResult(result.join("<br/>"));
        })
      });

  }

  return (
    <div>
      <div>
        <input type = "text" placeholder = "Search..."
          onChange={event => setQueryString(event.target.value)} />
        <button onClick={searchHandler}>Search</button>
      </div>
      <output>
        Result:
        <br /><br />{result}<br /><br /><br /> <br />
        Query String:
        <br /><br />{queryString}<br /><br />
      </output>
    </div>
  );
}

ReactDOM.render(<MyApp />, document.querySelector('#root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

当我尝试从 api 响应中搜索(过滤)学生时,在第二次单击时按 studentName 进行搜索,它因 result.push() 类型错误而中断。

我想要实现的行为:

  1. 单击 search 按钮后,我想按 studentName
  2. 过滤 api 数据
  3. 如果 queryString 为空,我想显示所有未过滤的项目
  4. 我想显示带有 <br/> 标记的结果

您应该将您的新价值传播到您所在的州 setter。 也就是说:

setResult(result.push(data[element].studentName));

应该是

setResult([...result, data[element].studentName]);

此外,我希望您打算在 filter 块中使用 element 而不是 data[element]。 虽然你的过滤器逻辑也不正确。您的回调函数用于 return 未设置状态的布尔结果。也许你打算做 Object.keys(data).forEach(...

让我们通过一些观察来了解为什么原始代码不能按预期工作

过滤不当

原版SearchHandler你有这篇

(data => {
  Object.keys(data).filter(element => {
    if (queryString === "") {
      setResult(result.push(data[element].studentName));
    } else if (data[element].studentName.toLowerCase().includes(queryString.toLowerCase())) {
      setResult(result.push(data[element].studentName));
    }
    setResult(result.join("<br/>"));
  })
});

所以 Object.keys(data) 获取了 data 的所有键,然后 filter 遍历了每个键,return 什么都没有。

filter 回调应该是 return 一个布尔值,以便 filter 方法可以决定存储数组中的哪些项,不存储哪些项。

然后理想情况下,您应该对 returned 过滤数组做一些理想的事情,这在此处不会发生。

最后post我会建议到底是什么。

使用错误的结果类型设置状态

接下来重点说一下这部分

  setResult(result.push(data[element].studentName));

这里发生了两件事:

  • result.push(data[element].studentName)即return是push后新数组的元素个数。为了举例,我们假设它是 5
  • 然后你用 5 调用 setResultsetResult(5)

我认为这不是你想要坚持的 result 状态。但它变得更深。

setState 调度注意事项

让我们稍微了解一下 React 调度 setState 调用的方式,以便为我们的进一步解释奠定基础

如需更深入的解答,请在此处查看详细信息

但本质上,在撰写本文时,React (v17) 当前仅在事件处理程序主体中直接调用时才对同一异步回调中发生的多个同级 setState 调用进行批处理(如 onClickonChange、...等)或直接在钩子主体中(如 useEffect)。

要形象化概念,请查看此示例中的控制台

function App() {
  const renderNumberRef = React.useRef(1);
  React.useEffect(() => {
    renderNumberRef.current += 1;
  });

  const [result, setResult] = React.useState([]);

  function searchHandler() {
    Promise.resolve().then(() => {
      console.log("enter searchHandler() promise callback");
      console.log("setResult() with student1");
      setResult([...result, "student1"]);
      console.log("setResult() with student2");
      setResult([...result, "student2"]);
      console.log("setResult() with student3");
      setResult([...result, "student3"]);
      console.log("exit searchHandler() promise callback");
    });
  }

  console.log(
    "Active render number: ",
    renderNumberRef.current,
    "state.result: ",
    result
  );

  return (
    <div>
      <div>
        <button onClick={searchHandler}>
          <div>Search Icon</div>
        </button>
      </div>
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('#root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

在你的例子中,setResult() 是在 searchHandler 内部调用的,它确实是一个事件处理程序,但它不在其直接主体中,它实际上是在嵌套的 fetch().then() promise 回调中.

这就是为什么 React 不会在每次 setResult() 调用时立即重新渲染组件,这将一遍又一遍地将值 5 保存到状态中,直到 searchHandler promise 回调结束

固定参考

现在您可能会问,为什么要 5

我们现在不是应该在下一个 setResult() 调用时中断 setResult(5.push()) 吗?

如果不是因为我们在 searchHandler 中使用了对同一个 result 变量的闭包引用,我们会的,所以你实际上做的是保持 setResult()- 一遍又一遍地使用相同的初始空数组,直到 searchHandler 函数完成执行。

并且每次在 searchHandler 执行期间发生重新渲染,我们都会将相同的值 5 重新分配给状态

错误

下次您再次单击搜索按钮时,您现在会选择 result,其中现在包含 5

并且逻辑立即中断,因为 5.push() 是类型错误

可能的解决方案

正如您所看到的,同一时间同一地点出现许多问题。

因此,首先您需要确保使用数组过滤,其主要目的是return过滤数组。

你拿那个数组,在过滤完成后把它保存到状态一次。

在过滤器内或您的活动仍在进行时多次调用 setResult 是次优的,不符合您的目的。

您应该始终尝试减少不必要的重新渲染。

应该有帮助的简单心智模型是:

  1. 获取一些数据
  2. transform/filter数据
  3. 将最终结果设置为状态
  4. 使用状态的结果在屏幕上渲染一些东西

所以我创建了一个重写和模拟的初始代码版本,并在内联注释中进行了解释。

const mockFetch = () =>
  Promise.resolve({
    json: () =>
      new Promise((resolve) => setTimeout(() => resolve({
        element1: { studentName: "student1" },
        element2: { studentName: "student2" },
        element3: { studentName: "student3" }
        // imitating 250ms delay with mocked fetch here
      }), 250))
  });

function App() {
  const [result, setResult] = React.useState([]);
  const [queryString, setQueryString] = React.useState("");

  function searchHandler(capturedQueryString) {
    mockFetch("{url that i defined in real code}")
      .then((response) => response.json())
      .then((data) => {
        // for each search you do not need to run setResult on
        // each iteration like in your initial code above in filter method
        // you just need to calculate once your whole state per request and then
        // set the result in state
        if (capturedQueryString === "") {
          // pay attention here we don't setResult(element.push())
          // because array.push() returns an integer that
          // signifies the length of the new array
          // so in your code above you were persisting the number itself
          // and then calling push() method of a number which fails
          // so instead we persist the array itself
          setResult(
            Object.keys(data).map((element) => data[element].studentName)
          );
        } else {
          setResult(
            Object.keys(data)
              .filter((element) => {
                return data[element].studentName
                  .toLowerCase()
                  .includes(capturedQueryString.toLowerCase());
              })
              .map((filteredElement) => data[filteredElement].studentName)
          );
        }
      });
  }

  return (
    <div>
      <div>
        <input
          type="text"
          id="search-bar"
          placeholder="Search..."
          onChange={(event) => setQueryString(event.target.value)}
        />

        {/* passing in captured queryString
        // so that when clicked within searchHandler
        // we always operate with the same queryString
        // regardless if queryString has changed in state or not
        */}
        <button onClick={() => searchHandler(queryString)}>
          <div>Search Icon</div>
        </button>
      </div>

      <output>
        Result:
        <br />
        <br />
        {/* In order to join your array with <br /> tag you need to inner html it */} 
        {/* React provides that possibility via dangerouslySetInnerHTML api */}
        {/* BUT since your student name can be hijacked by attacker given that your api is also not sanitized */} 
        {/* the best practice is to sanitize the joined string with something like sanitize-html or DOMPurify package */}
        <span dangerouslySetInnerHTML={{__html: result.join('<br />')}} />
        <br />
        <br />
        <br />
        <br />
        Query String:
        <br />
        <br />
        {queryString}
        <br />
        <br />
      </output>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id="root"></div>


P.S. 当然这不是该功能的最终版本,因为它没有涵盖过多的边缘情况,例如(如果您单击3 次,第 2 个请求在第 3 个之后解决,或者如果其中一个请求被拒绝怎么办等等)但这应该是一个好的开始并且应该有望解除对您的阻止。