React-Select 异步加载选项未正确加载选项

React-Select Async loadOptions is not loading options properly

React Async Select 加载选项有时无法加载选项。这是一个非常奇怪的现象,在几组查询反应加载选项没有加载任何值之后,但我可以从日志中看到结果正确地来自后端查询。我的代码库完全与 react-select 新版本和使用

保持同步

"react-select": "^2.1.1"

这是我的 react-async select 组件的前端代码。我确实在我的 getOptions 函数中使用了 debounce 来减少后端搜索查询的数量。我想这应该不会造成任何问题。我想补充一点,我在这种情况下观察到,loadoptions serach indicator ( ... ) 也没有出现在这种现象中。

import React from 'react';
import AsyncSelect from 'react-select/lib/Async';
import Typography from '@material-ui/core/Typography';
import i18n from 'react-intl-universal';

const _ = require('lodash');

class SearchableSelect extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: '',
      searchApiUrl: props.searchApiUrl,
      limit: props.limit,
      selectedOption: this.props.defaultValue
    };
    this.getOptions = _.debounce(this.getOptions.bind(this), 500);
    //this.getOptions = this.getOptions.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.noOptionsMessage = this.noOptionsMessage.bind(this);
    this.handleInputChange = this.handleInputChange.bind(this);
  }

  handleChange(selectedOption) {
    this.setState({
      selectedOption: selectedOption
    });
    if (this.props.actionOnSelectedOption) {
      // this is for update action on selectedOption
      this.props.actionOnSelectedOption(selectedOption.value);
    }
  }

  handleInputChange(inputValue) {
    this.setState({ inputValue });
    return inputValue;
  }

  async getOptions(inputValue, callback) {
    console.log('in getOptions'); // never print
    if (!inputValue) {
      return callback([]);
    }
    const response = await fetch(
      `${this.state.searchApiUrl}?search=${inputValue}&limit=${
        this.state.limit
      }`
    );
    const json = await response.json();
    console.log('results', json.results); // never print
    return callback(json.results);
  }

  noOptionsMessage(props) {
    if (this.state.inputValue === '') {
      return (
        <Typography {...props.innerProps} align="center" variant="title">
          {i18n.get('app.commons.label.search')}
        </Typography>
      );
    }
    return (
      <Typography {...props.innerProps} align="center" variant="title">
        {i18n.get('app.commons.errors.emptySearchResult')}
      </Typography>
    );
  }
  getOptionValue = option => {
    return option.value || option.id;
  };

  getOptionLabel = option => {
    return option.label || option.name;
  };

  render() {
    const { defaultOptions, placeholder } = this.props;
    return (
      <AsyncSelect
        cacheOptions
        value={this.state.selectedOption}
        noOptionsMessage={this.noOptionsMessage}
        getOptionValue={this.getOptionValue}
        getOptionLabel={this.getOptionLabel}
        defaultOptions={defaultOptions}
        loadOptions={this.getOptions}
        placeholder={placeholder}
        onChange={this.handleChange}
      />
    );
  }
}

export default SearchableSelect;

编辑以回应史蒂夫的回答

谢谢史蒂夫的回答。仍然没有运气。我尽量按照你的回复点来回复。

  1. 如果我不使用 optionsValue,而是使用 getOptionValue 和 getOptionLevel,则查询结果不会正确加载。我的意思是加载了空白选项,没有文本值。
  2. 是的,你是对的,是一个返回字符串的同步方法,我不需要覆盖它。这工作正常并且 noOptionsMessage 显示正确。感谢指出这一点。
  3. actionOnSelectedOption 不是 noop 方法,它可能有一些责任要执行。我尝试使用 SearchableSelect 作为一个独立的组件,如果我需要一些后端操作来执行此功能将相应地触发它。例如,我在我的项目的用户配置文件中使用它,用户可以在其中根据现有条目更新他的 school/college 信息。当用户 select 一个选项时,有一个配置文件更新责任要执行。
  4. 是的,你是对的。我不需要将 inputValue 保持在状态,谢谢。
  5. 我确保 defaultOptions 是一个数组。
  6. 我在没有使用去抖动的情况下进行了测试,仍然没有运气。我正在使用 debounce 来限制后端调用,否则我肯定不想要的每个击键都可能有后端调用。

async select 对于 2/3 的查询完美工作,然后突然停止工作。我观察到一种可区分的行为,对于那些情况,搜索指示器 ( ... ) 也没有显示。

非常感谢您抽出时间。

编辑 2 以回应史蒂夫的回答

再次感谢您的回复。我对 getOptionValue 和 getOptionLabel 的理解是错误的。如果 loadOptions 得到响应,这两个函数都会被调用。因此,我从我之前的代码片段中删除了我的辅助 optionsValue 函数,并根据(In this post also)更新了我的代码片段。但仍然没有运气。在某些情况下,async-select 不起作用。我试着截取一个这样的案例。我确实在本地数据库名称 "tamim johnson" 中使用了名称,但是当我搜索他时,我没有得到任何响应,但从后端得到了正确的响应。这是这个案例的截图

我不确定这个截图有多清晰。 Tamim johnson 在我的排名中也排在第 6 位。

谢谢先生抽出宝贵时间。我不知道我做错了什么或遗漏了什么。

编辑 3 以回应史蒂夫的回答

这是名为 "tamim johnson" 的用户搜索的预览选项卡响应。

一些注释可以在代码下方找到。您正在寻找这样的东西:

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import AsyncSelect from 'react-select/lib/Async';
import debounce from 'lodash.debounce';
import noop from 'lodash.noop';
import i18n from 'myinternationalization';

const propTypes = {
  searchApiUrl: PropTypes.string.isRequired,
  limit: PropTypes.number,
  defaultValue: PropTypes.object,
  actionOnSelectedOption: PropTypes.func
};

const defaultProps = {
  limit: 25,
  defaultValue: null,
  actionOnSelectedOption: noop
};

export default class SearchableSelect extends Component {
  static propTypes = propTypes;
  static defaultProps = defaultProps;
  constructor(props) {
    super(props);
    this.state = {
      inputValue: '',
      searchApiUrl: props.searchApiUrl,
      limit: props.limit,
      selectedOption: this.props.defaultValue,
      actionOnSelectedOption: props.actionOnSelectedOption
    };
    this.getOptions = debounce(this.getOptions.bind(this), 500);
    this.handleChange = this.handleChange.bind(this);
    this.noOptionsMessage = this.noOptionsMessage.bind(this);
    this.handleInputChange = this.handleInputChange.bind(this);
  }

  getOptionValue = (option) => option.id;

  getOptionLabel = (option) => option.name;

  handleChange(selectedOption) {
    this.setState({
      selectedOption: selectedOption
    });
    // this is for update action on selectedOption
    this.state.actionOnSelectedOption(selectedOption.value);
  }

  async getOptions(inputValue) {
    if (!inputValue) {
      return [];
    }
    const response = await fetch(
      `${this.state.searchApiUrl}?search=${inputValue}&limit=${
      this.state.limit
      }`
    );
    const json = await response.json();
    return json.results;
  }

  handleInputChange(inputValue) {
    this.setState({ inputValue });
    return inputValue;
  }

  noOptionsMessage(inputValue) {
    if (this.props.options.length) return null;
    if (!inputValue) {
      return i18n.get('app.commons.label.search');
    }

    return i18n.get('app.commons.errors.emptySearchResult');
  }

  render() {
    const { defaultOptions, placeholder } = this.props;
    const { selectedOption } = this.state;
    return (
      <AsyncSelect
        cacheOptions
        value={selectedOption}
        noOptionsMessage={this.noOptionsMessage}
        getOptionValue={this.getOptionValue}
        getOptionLabel={this.getOptionLabel}
        defaultOptions={defaultOptions}
        loadOptions={this.getOptions}
        placeholder={placeholder}
        onChange={this.handleChange}
      />
    );
  }
}
  1. 您不需要该方法来映射您的结果集。有道具 为你处理。
  2. 如果您的 i18n.get() 是一个同步方法 returning 一个字符串,您不必覆盖整个组件(即使是样式更改)
  3. 如果您将 actionOnSelectedOption 默认为 noop 方法,那么您不再 需要有条件才能调用它。
  4. React-Select 在内部跟踪 inputValue。除非你有一些外部需求(你的包装器),否则不需要尝试管理它的状态。
  5. defaultOptions 要么
    • 一组默认选项(在过滤之前不会调用 loadOptions
    • true(将从您的 loadOptions 方法自动加载)
  6. Async/Await 函数 return 承诺,使用承诺响应而不是 callback 类型。

我想知道,通过在 debounce 中包装您的 getOptions() 方法,您是否打破了组件的 this 范围。不能肯定地说,因为我以前从未使用过 debounce。您可以提取该包装器并尝试使用您的代码进行测试。

问题是Lodash的debounce函数不适合这个。 Lodash 指定

subsequent calls to the debounced function return the result of the last func invocation

不是那个:

subsequent calls return promises which will resolve to the result of the next func invocation

这意味着在等待期内对去抖动的 loadOptions prop 函数的每次调用实际上都返回最后一次 func 调用,因此我们关心的 "real" 承诺从未被订阅。

而是使用承诺返回的去抖功能

例如:

import debounce from "debounce-promise";

//...
this.getOptions = debounce(this.getOptions.bind(this), 500);

查看完整说明https://github.com/JedWatson/react-select/issues/3075#issuecomment-450194917

我发现有人打算找这个问题。所以我发布了解决问题的代码更新部分。从异步等待转换为正常回调函数解决了我的问题。特别感谢史蒂夫和其他人。

import React from 'react';
import AsyncSelect from 'react-select/lib/Async';
import { loadingMessage, noOptionsMessage } from './utils';
import _ from 'lodash';

class SearchableSelect extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      selectedOption: this.props.defaultValue
    };
    this.getOptions = _.debounce(this.getOptions.bind(this), 500);
  }

  handleChange = selectedOption => {
    this.setState({
      selectedOption: selectedOption
    });
    if (this.props.actionOnSelectedOption) {
      this.props.actionOnSelectedOption(selectedOption.value);
    }
  };

  mapOptionsToValues = options => {
    return options.map(option => ({
      value: option.id,
      label: option.name
    }));
  };

  getOptions = (inputValue, callback) => {
    if (!inputValue) {
      return callback([]);
    }

    const { searchApiUrl } = this.props;
    const limit =
      this.props.limit || process.env['REACT_APP_DROPDOWN_ITEMS_LIMIT'] || 5;
    const queryAdder = searchApiUrl.indexOf('?') === -1 ? '?' : '&';
    const fetchURL = `${searchApiUrl}${queryAdder}search=${inputValue}&limit=${limit}`;

    fetch(fetchURL).then(response => {
      response.json().then(data => {
        const results = data.results;
        if (this.props.mapOptionsToValues)
          callback(this.props.mapOptionsToValues(results));
        else callback(this.mapOptionsToValues(results));
      });
    });
  };

  render() {
    const { defaultOptions, placeholder, inputId } = this.props;
    return (
      <AsyncSelect
        inputId={inputId}
        cacheOptions
        value={this.state.selectedOption}
        defaultOptions={defaultOptions}
        loadOptions={this.getOptions}
        placeholder={placeholder}
        onChange={this.handleChange}
        noOptionsMessage={noOptionsMessage}
        loadingMessage={loadingMessage}
      />
    );
  }
}

export default SearchableSelect;