在 React 中同步事件处理程序以进行搜索

Synchronising Event Handler for Search in React

我一直在学习 js,然后在过去几周里 React.js,遵循 Codecademy 上的教程,然后 Educative.io(使用新的 hooks 学习,而不是 class基于方法)。为了尝试应用我所学的知识,我一直在忙于创建一些常见的网站功能作为 hello-world 项目中的 React 组件。

最近我一直在尝试制作一个搜索组件,它使用 Spotify API 来搜索曲目,但是 运行 遇到了我不太清楚的同步问题了解如何使用我知道的 js 同步工具来解决。我来自 Java 背景,所以对 mutexes/semaphores/reader-writer locks/monitors 更熟悉,所以我可能遗漏了一些明显的东西。我的代码一直基于 this 博客 post。

在我的实现中,我目前有一个 SongSearch 组件,它作为 属性 传递其初始搜索文本,以及一个调用的回调函数当输入值改变时。它还包含 searchText 作为状态,用于更改输入的值。

import * as React from 'react';

interface Props {
    initialSearchText: string,
    onSearchTextUpdated: (newSearchText: string) => void;
}

export const SongSearch = (props: Props) => {
    const [searchText, setSearchText] = React.useState(props.initialSearchText);
    const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const newSearchText = e.target.value;
        setSearchText(newSearchText);
        props.onSearchTextUpdated(newSearchText);
    }
    return <input value={searchText} onChange={onChange}/>;
};

结果目前仅在 SearchResults 组件中显示一个列表,其值作为歌曲数组传递。

import * as React from 'react';

import { SongInfo } from './index';

interface Props {
    songs: SongInfo[]
}

export const SearchResults = (props: Props) => {
    return (
        <ul>
            {props.songs.map((song) => {
                return <li key={song.uri}>{song.name}</li>
            })}
        </ul>
    );
}

App 组件中,我传递了一个回调函数,它将状态属性 searchText 设置为新值。然后这会触发调用 updateSongs() 的效果。如果我们有一个授权令牌,并且搜索文本不为空,我们 return 调用 API 的结果,否则我们 return 一个空的歌曲列表。结果用于使用 setTracks() 更新状态的轨道属性。

我已将 App.tsx 中的代码缩减为仅相关部分:

import SpotifyWebApi from 'spotify-web-api-js';
import React from "react";

// ... (removed irrelevant code)

async function updateSongs(searchText: string): Promise<SongInfo[]>{
  if (spotify.getAccessToken()) {
    if (searchText === '') {
      console.log('Empty search text.');
      return [];
    } else {
      // if access token has been set 
      const res = await spotify.searchTracks(searchText, {limit: 10});
      const tracks = res.tracks.items.map((trackInfo) => { 
        return {name: trackInfo.name, uri: trackInfo.uri};
      });
      console.log(tracks);
      return tracks;
    }

  } else {
    console.log('Not sending as access token has not yet');
    return [];
  }
}

function App() {
  // ... (removed irrelevant code)

  const initialSearchText = 'Search...';

  const [tracks, setTracks] = React.useState([] as SongInfo[]);

  const [searchText, setSearchText] = React.useState(initialSearchText);

  React.useEffect(() => {
    updateSongs(searchText)
      .then((newSongs) => setTracks(newSongs))
  }, [searchText]);

  const content = <SearchResults songs={tracks}/>;
  return (
    <ThemeProvider theme={theme}>
      <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
        <Root config={mui_config}>
          <Header
            renderMenuIcon={(open: boolean) => (open ? <ChevronLeft /> : <MenuRounded />)}
          >
            <SongSearch initialSearchText={initialSearchText} onSearchTextUpdated={(newSearchText) =>  {
              console.log(`New Search Text: ${newSearchText}`)
              setSearchText(newSearchText);
            }}/>
          </Header>
          <Nav
            renderIcon={(collapsed: boolean)=>
              collapsed ? <ChevronRight /> : <ChevronLeft />
            }
            classes={drawerStyles}
          >
            Nav
          </Nav>
          <StickyFooter contentBody={content} footerHeight={100} footer={footerContent}/>

        </Root>
      </div>
    </ThemeProvider>
  );
}


export default App;

我遇到的问题是,当我输入一首长歌曲的名称然后按住退格键时,有时即使搜索文本为空,歌曲仍会显示在列表中。通过检查代码中的控制台日志,我可以看到问题的出现是因为 setTracks() 有时被乱序调用,特别是在快速删除 'abcdef' setTracks() updateTracks('a') 将在 updateTracks('') 的结果之后被调用。这是有道理的,因为 '' 不需要任何网络流量,但我花了几个小时试图找出如何在 javascript 中同步它,但无济于事。

如有任何帮助,我们将不胜感激!

在您的情况下,返回的结果不同,因为您发送了多个事件,而最先出现的事件 - 触发响应然后显示它。

我的解决方案是在输入字段的 onChange 事件上使用 debounce 函数。这样用户将首先完成输入,然后它应该开始搜索。尽管可能仍然存在一些问题,但如果一个搜索已经开始并且用户开始输入其他内容,那么第一个搜索已经完成,第二个搜索已经开始并完成。在此您可能会发现取消请求很有帮助。不幸的是,您无法取消 Promise,因此您必须阅读有关 RxJS 的内容。

这是一个 working example 使用 debounce

P.S。 您可能会发现此 conference talk 有助于理解事件循环在 JS 中的工作方式。