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);
   });
}
Другие вопросы по тегам