Reactjs и redux - Как предотвратить чрезмерные вызовы API из компонента live-поиска?
Я создал этот компонент поиска в реальном времени:
class SearchEngine extends Component {
constructor (props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSearch = this.handleSearch.bind(this);
}
handleChange (e) {
this.props.handleInput(e.target.value); //Redux
}
handleSearch (input, token) {
this.props.handleSearch(input, token) //Redux
};
componentWillUpdate(nextProps) {
if(this.props.input !== nextProps.input){
this.handleSearch(nextProps.input, this.props.loginToken);
}
}
render () {
let data= this.props.result;
let searchResults = data.map(item=> {
return (
<div key={item.id}>
<h3>{item.title}</h3>
<hr />
<h4>by {item.artist}</h4>
<img alt={item.id} src={item.front_picture} />
</div>
)
});
}
return (
<div>
<input name='input'
type='text'
placeholder="Search..."
value={this.props.input}
onChange={this.handleChange} />
<button onClick={() => this.handleSearch(this.props.input, this.props.loginToken)}>
Go
</button>
<div className='search_results'>
{searchResults}
</div>
</div>
)
}
Он является частью приложения React & Redux, над которым я работаю, и связан с магазином Redux. Дело в том, что когда пользователь вводит в поисковый запрос, он запускает вызов API для каждого из символов во входных данных и создает чрезмерный вызов API, что приводит к ошибкам, таким как отображение результатов предыдущих запросов, а не отслеживание текущего поисковый ввод.
Мой вызов API (this.props.handleSearch):
export const handleSearch = (input, loginToken) => {
const API= `https://.../api/search/vector?query=${input}`;
}
return dispatch => {
fetch(API, {
headers: {
'Content-Type': 'application/json',
'Authorization': loginToken
}
}).then(res => {
if (!res.ok) {
throw Error(res.statusText);
}
return res;
}).then(res => res.json()).then(data => {
if(data.length === 0){
dispatch(handleResult('No items found.'));
}else{
dispatch(handleResult(data));
}
}).catch((error) => {
console.log(error);
});
}
};
Мое намерение состоит в том, чтобы это был живой поиск и обновление самого себя на основе пользовательского ввода. но я пытаюсь найти способ дождаться, пока пользователь закончит ввод, а затем применить изменения, чтобы избежать чрезмерного вызова API и ошибок.
Предложения?
РЕДАКТИРОВАТЬ:
Вот что сработало для меня. Благодаря удивительному ответу Hammerbot мне удалось создать свой собственный класс QueueHandler.
export default class QueueHandler {
constructor () { // not passing any "queryFunction" parameter
this.requesting = false;
this.stack = [];
}
//instead of an "options" object I pass the api and the token for the "add" function.
//Using the options object caused errors.
add (api, token) {
if (this.stack.length < 2) {
return new Promise ((resolve, reject) => {
this.stack.push({
api,
token,
resolve,
reject
});
this.makeQuery()
})
}
return new Promise ((resolve, reject) => {
this.stack[1] = {
api,
token,
resolve,
reject
};
this.makeQuery()
})
}
makeQuery () {
if (! this.stack.length || this.requesting) {
return null
}
this.requesting = true;
// here I call fetch as a default with my api and token
fetch(this.stack[0].api, {
headers: {
'Content-Type': 'application/json',
'Authorization': this.stack[0].token
}
}).then(response => {
this.stack[0].resolve(response);
this.requesting = false;
this.stack.splice(0, 1);
this.makeQuery()
}).catch(error => {
this.stack[0].reject(error);
this.requesting = false;
this.stack.splice(0, 1);
this.makeQuery()
})
}
}
Я сделал несколько изменений, чтобы это работало на меня (см. Комментарии).
Я импортировал его и назначил переменную:
//searchActions.js file which contains my search related Redux actions
import QueueHandler from '../utils/QueueHandler';
let queue = new QueueHandler();
Тогда в моей оригинальной функции handleSearch:
export const handleSearch = (input, loginToken) => {
const API= `https://.../api/search/vector?query=${input}`;
}
return dispatch => {
queue.add(API, loginToken).then... //queue.add instead of fetch.
Надеюсь, это кому-нибудь поможет!
1 ответ
Я думаю, что это несколько стратегий для решения проблемы. Я собираюсь поговорить о трех способах здесь.
Двумя первыми способами являются "регулирование" и "отмена" вашего ввода. Здесь есть очень хорошая статья, которая объясняет различные методы: https://css-tricks.com/debouncing-throttling-explained-examples/
Debounce ожидает заданного времени для фактического выполнения функции, которую вы хотите выполнить. И если за это время вы сделаете тот же звонок, он снова будет ждать в данное время, чтобы увидеть, не позвоните ли вы снова. Если вы этого не сделаете, он выполнит функцию. Это объясняется этим изображением (взято из статьи, упомянутой выше):
Throttle выполняет функцию напрямую, ожидает в течение заданного времени нового вызова и выполняет последний вызов, сделанный за это время. Следующая схема объясняет это (взято из этой статьи http://artemdemo.me/blog/throttling-vs-debouncing/):
Сначала я использовал эти первые приемы, но нашел некоторые недостатки в этом. Основным было то, что я не мог контролировать рендеринг моего компонента.
Давайте представим следующую функцию:
function makeApiCall () {
api.request({
url: '/api/foo',
method: 'get'
}).then(response => {
// Assign response data to some vars here
})
}
Как видите, в запросе используется асинхронный процесс, который позже назначит данные ответа. Теперь давайте представим два запроса, и мы всегда хотим использовать результат последнего запроса, который был выполнен. (Это то, что вы хотите в поиске ввода). Но результат второго запроса предшествует первому запросу. Это приведет к тому, что ваши данные будут содержать неправильный ответ:
1. 0ms -> makeApiCall() -> 100ms -> assigns response to data
2. 10ms -> makeApiCall() -> 50ms -> assigns response to data
Для меня решением было создать какую-то "очередь". Поведение этой очереди:
1 - Если мы добавляем задачу в очередь, задача идет перед очередью. 2 - Если мы добавим в очередь второе задание, задание перейдет на вторую позицию. 3 - Если мы добавляем третье задание в очередь, задание заменяет второе.
Таким образом, в очереди есть максимум две задачи. Как только первая задача закончилась, вторая задача выполнена и т. Д...
Таким образом, у вас всегда один и тот же результат, и вы ограничиваете свои вызовы API в зависимости от многих параметров. Если у пользователя медленное интернет-соединение, выполнение первого запроса займет некоторое время, поэтому запросов будет не много.
Вот код, который я использовал для этой очереди:
export default class HttpQueue {
constructor (queryFunction) {
this.requesting = false
this.stack = []
this.queryFunction = queryFunction
}
add (options) {
if (this.stack.length < 2) {
return new Promise ((resolve, reject) => {
this.stack.push({
options,
resolve,
reject
})
this.makeQuery()
})
}
return new Promise ((resolve, reject) => {
this.stack[1] = {
options,
resolve,
reject
}
this.makeQuery()
})
}
makeQuery () {
if (! this.stack.length || this.requesting) {
return null
}
this.requesting = true
this.queryFunction(this.stack[0].options).then(response => {
this.stack[0].resolve(response)
this.requesting = false
this.stack.splice(0, 1)
this.makeQuery()
}).catch(error => {
this.stack[0].reject(error)
this.requesting = false
this.stack.splice(0, 1)
this.makeQuery()
})
}
}
Вы можете использовать это так:
// First, you create a new HttpQueue and tell it what to use to make your api calls. In your case, that would be your "fetch()" function:
let queue = new HttpQueue(fetch)
// Then, you can add calls to the queue, and handle the response as you would have done it before:
queue.add(API, {
headers: {
'Content-Type': 'application/json',
'Authorization': loginToken
}
}).then(res => {
if (!res.ok) {
throw Error(res.statusText);
}
return res;
}).then(res => res.json()).then(data => {
if(data.length === 0){
dispatch(handleResult('No vinyls found.'));
}else{
dispatch(handleResult(data));
}
}).catch((error) => {
console.log(error);
});
}