web-dev-qa-db-fra.com

React-Select Async loadOptions ne charge pas les options correctement

Réaction asynchrone L'option de chargement échoue parfois pour charger l'option. Il s’agit d’un phénomène très étrange après que plusieurs requêtes aient réagi. Loadoptions ne charge aucune valeur, mais je peux voir dans le journal que les résultats proviennent correctement d’une requête. Ma base de code est totalement à jour avec réag-select nouvelle version et en utilisant 

"réagir-sélectionner": "^ 2.1.1"

Voici mon code frontal pour le composant react-async select. J'utilise debounce dans ma fonction getOptions pour réduire le nombre de requêtes de recherche d'arrière-plan. Cela ne devrait poser aucun problème, je suppose. Je voudrais ajouter un autre point que j'observe dans ce cas, l'indicateur loadoptions serach (...) n'apparaît pas non plus dans ce phénomène. 

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;

Modifier pour répondre à la réponse de Steve

Merci pour votre réponse Steve. Toujours pas de chance. J'essaie de répondre en fonction de vos points de réponse. 

  1. Si je n'utilise pas optionsValue, utilisez plutôt getOptionValue et getOptionLevel, alors le résultat de la requête ne sera pas chargé correctement. Je veux dire là des options vides chargés, aucune valeur de texte. 
  2. oui vous avez raison, est une méthode synchrone renvoyant une chaîne, je n'ai pas besoin de remplacer cela. Et cela fonctionne bien et noOptionsMessage montre correctement. Merci de le signaler. 
  3. actionOnSelectedOption n'est pas une méthode noop, elle peut avoir une certaine responsabilité à exécuter. J'essaie d'utiliser SearchableSelect en tant que composant indépendant. Si j'ai besoin d'une action back-end pour faire cette fonction, cela déclenchera cela en conséquence. Par exemple, j'utilise ceci dans le profil utilisateur de mon projet, où l'utilisateur peut mettre à jour les informations de son école/collège à partir d'entrées existantes. Lorsque l'utilisateur sélectionne une option, il est responsable de la mise à jour du profil. 
  4. Oui, tu as raison. Je n'ai pas besoin de maintenir inputValue dans cet état, merci. 
  5. Je m'assure que defaultOptions est un tableau. 
  6. Je teste sans utiliser le rebond, toujours pas de chance. J'utilise debounce pour limiter l'appel backend, sinon il peut y avoir un appel backend pour chaque coup de touche que je ne veux sûrement pas. 

async select fonctionne parfaitement pour 2/3 requêtes et cesse ensuite de fonctionner. Un comportement pouvant être distingué, j’observe que, dans ces cas, les indicateurs de recherche (...) ne sont pas non plus indiqués. 

Merci beaucoup pour votre temps. 

Modifier 2 pour répondre à la réponse de Steve

Merci beaucoup pour votre réponse à nouveau. Je me suis trompé à propos de getOptionValue et de getOptionLabel. Si loadOptions obtient une réponse, ces deux fonctions sont appelées. J'ai donc retiré la fonction optionsValue de mon assistant de mon extrait de code précédent et mis à jour mon extrait de code en fonction de (dans cet article également). Mais toujours pas de chance. Dans certains cas, async-select ne fonctionnait pas. J'essaie de prendre une capture d'écran d'un tel cas. Je ne nomme pas utiliser dans mon nom local-db "tamim johnson" mais quand je le fouille je ne reçois aucune réponse, mais je reçois une réponse correcte en retour. Voici la capture d'écran de cette affaire  tamim johnson

Je ne sais pas à quel point cette capture d'écran est claire. Tamim Johnson également en 6ème position dans mon classement. 

Merci monsieur pour votre temps. Je n'ai aucune idée de ce que je fais mal ou manque quelque chose. 

Modifier 3 pour répondre à la réponse de Steve

Ceci est la réponse de l'onglet d'aperçu pour la recherche de l'utilisateur nommé "tamim johnson". 

 preview tab

3
Shakil

J'ai découvert que les gens ont l'intention de rechercher ce problème. Donc, je publie ma partie mise à jour du code qui résout le problème. La conversion de asynchrone-wait en une fonction de rappel normale corrige mon problème. Un merci spécial à Steve et aux autres. 

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;
1
Shakil

Certaines notes peuvent être trouvées sous le code. Vous cherchez quelque chose comme ça:

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. Vous n'avez pas besoin de la méthode pour mapper votre jeu de résultats. Il existe des accessoires Qui gèrent cela pour vous. 
  2. Si votre i18n.get() est une méthode synchrone renvoyant une chaîne, vous n'avez pas à remplacer tout le composant (même pour les modifications de style). 
  3. Si vous définissez par défaut votre actionOnSelectedOption sur une méthode noop, vous n'avez plus besoin de Pour pouvoir l'appeler.
  4. React-Select suit inputValue en interne. À moins que vous n'ayez des besoins externes (votre wrapper), il n'est pas nécessaire d'essayer de gérer son état.
  5. defaultOptions est soit
    • un tableau d'options par défaut (n'appelle pas la loadOptions tant que vous n'avez pas filtré)
    • true (chargera automatiquement à partir de votre méthode loadOptions)
  6. Les fonctions Async/Await renvoient une promesse, en utilisant la réponse de promesse plutôt que le type callback.

Je me demande si, en encapsulant votre méthode getOptions() dans debounce, vous rompez la portée this avec votre composant. Je ne peux pas dire avec certitude, car je n’avais jamais utilisé debounce auparavant. Vous pouvez tirer ce wrapper et essayer votre code à tester.

3

Le problème est que la fonction anti-rebond de Lodash ne convient pas à cela. Lodash précise que

les appels suivants à la fonction debounce renvoient le résultat du dernière invocation de fonction

Pas ça:

les appels suivants renvoient des promesses qui aboutiront au résultat de la prochaine invocation de fonction

Cela signifie que chaque appel entrant dans la période d'attente de la fonction debO debounce de loadOptions renvoie en réalité la dernière invocation de fonction, de sorte que la "vraie" promesse dont nous nous soucions n'est jamais souscrite.

Utilisez plutôt une fonction anti-rebond à retour de promesse 

Par exemple:

import debounce from "debounce-promise";

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

Voir l'explication complète https://github.com/JedWatson/react-select/issues/3075#issuecomment-450194917

1
craigmichaelmartin