在 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 中的工作方式。
我一直在学习 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 中的工作方式。