Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How Can I Setup `react-select` to work correctly with server-side data by using AsyncSelect?

I would like to setup a component react-select to work server-side data and do server-side filtering, but it doesn't work for a plethora of reasons.

Can you explain it and also show working code?

like image 570
mgPePe Avatar asked Oct 30 '25 15:10

mgPePe


1 Answers

Let's start by me expressing the opinion that react-select seems great, but not very clearly documented. Personally I didn't fall in love with the documentation for the following reasons:

  • No search
  • All the props and put on a single page. If I do CTRL+F on something everything lights up. Pretty useless
  • Most descriptions are minimal and not describing the important edge cases, some are even missing
  • There are some examples, but not nearly enough to show the different varieties, so you have to do guesswork

And so I will try to help a bit with this article, by giving steps by steps, code and problems + solutions.

Step 1: Simplest form react-select:

const [options, setOptions] = useState([
    { id: 'b72a1060-a472-4355-87d4-4c82a257b8b8', name: 'illy' },
    { id: 'c166c9c8-a245-48f8-abf0-0fa8e8b934d2', name: 'Whiskas' },
    { id: 'cb612d76-a59e-4fba-8085-c9682ba2818c', name: 'KitKat' },
  ]);
 <Select
        defaultValue={options[0]}
        isClearable
        options={options}
        getOptionLabel={(option) => option.name}
        getOptionValue={(option) => option.id}
      />

It generally works, but you will notice that if I type the letter d which doesn't match any of the choices anywhere, choices stay, instead of showing "no options" as it should.

enter image description here

I will ignore this issue, since it is minor and seems unfixable.

So far so good, we can live with that small issue.

Step 2: Convert static data to server data

Our goal is now to simply swap the static data with server loaded data. Meh, how difficult could it be?

We will first need to swap <Select/> for <AsyncSelect/>. Now how do we load data?

So looking at the documentation there are multiple ways of loading data:

defaultOptions: The default set of options to show before the user starts searching. When set to true, the results for loadOptions('') will be autoloaded.

and

loadOptions: Function that returns a promise, which is the set of options to be used once the promise resolves.

Reading it carefully you understand defaultOptions needs to be a boolean value true and loadOptions should have a function returning the choices:

      <AsyncSelect
        defaultValue={options[0]}
        isClearable
        getOptionLabel={(option) => option.name}
        getOptionValue={(option) => option.id}
        defaultOptions
        loadOptions={loadData}
      />

Looks great, we have remote data loaded. But we want to preset our default value now. We have to match it by Id, rather than choosing the first one. Here comes our first problem:

PROBLEM: You can't set the defaultValue in the very beginning, because you have no data to match it against. And if you try to set the defaultValue after component has loaded, then it doesn't work.

To solve that, we need to load data in advance, match the initial value we have, and once we have both of those, we can initialize the component. A bit ugly but that's the only way I could figure it out given the limitations:

const [data, setData] = useState(null);
const [initialObject, setInitialObject] = useState(null);

const getInitial = async () => {
    // make your request, once you receive data:
    // Set initial object
    const init= res.data.find((item)=>item.id=ourInitialId);
    setInitialObject(init); 
    // Set data so component initializes
     setData(res.data);
  };
useEffect(() => {
    getInitial();
  }, []);

return (
     <>
      {data!== null && initialObject !== null ? (
        <AsyncSelect
          isClearable
          getOptionLabel={(option) => option.name}
          getOptionValue={(option) => option.id}
          defaultValue={initialObject}
          defaultOptions={options}
          // loadOptions={loadData} // we don't need this anymore
        />
      ) : null}
     </>
    )

Since we are loading the data ourselves, we don't need loadOptions so we will take it out. So far so good.

Step 3: Make filter with server-side filtering call

So now we need a callback that we can use for getting data. Let's look back at the documentation:

onChange: (no description, from section "StateManager Props")

onInputChange: Same behaviour as for Select

So we listen to documentation and go back to "Select Props" section to find:

onInputChange: Handle change events on the input`

Insightful...NOT.

We see a function types definition that seems to have some clues:

enter image description here

I figured, that string must by my text/query. And apparently it drops in the type of change. Off we go --

const [data, setData] = useState(null);
const [initialObject, setInitialObject] = useState(null);

const getInitial = async () => {
    // make your request, once you receive data:
    // Set initial object
    const init= res.data.find((item)=>item.id=ourInitialId);
    setInitialObject(init); 
    // Set data so component initializes
     setData(res.data);
  };
useEffect(() => {
    getInitial();
  }, []);

  const loadData = async (query) => {
   // fetch your data, using `query`
   return res.data;
  };

return (
     <>
      {data!== null && initialObject !== null ? (
        <AsyncSelect
          isClearable
          getOptionLabel={(option) => option.name}
          getOptionValue={(option) => option.id}
          defaultValue={initialObject}
          defaultOptions={options}
          onInputChange={loadData} // +
        />
      ) : null}
     </>
    )

Data gets fetched with the right query, but options don't update as per our server data results. We can't update the defaultOptions since it is only used during initialization, so the only way to go would be to bring back loadOptions. But once we do, we have 2 calls on every keystroke. Blak. By countless hours and miracle of painstaking experimentation, we now figure out that:

USEFUL REVELATION: loadOptions actually fires on inputChange, so we don't actually need onInputChange.

<AsyncSelect
  isClearable
  getOptionLabel={(option) => option.name}
  getOptionValue={(option) => option.id}
  defaultValue={initialObject}
  defaultOptions={options}
  // onInputChange={loadData} // remove that
  loadOptions={loadData} // add back that
/>

Things look good. Even our d search has automagically been fixed somehow:

enter image description here

Step 4: Update formik or whatever form value you have

To do that we need something that fires on select:

onChange: (no explanation or description)

Insightful...NOT. We have a pretty and colorful definition again to our rescue and we pick up some clues:

enter image description here

So we see the first param (which we don't know what it is can be object, array of array, null, or undefined. And then we have the types of actions. So with some guessing we figure out, it must be passing the selected object:

We will pass setFieldValue function as a prop to the component:

onChange={(selectedItem) => {
  setFieldValue(fieldName, selectedItem?.id); // fieldName is also passed as a prop
}}

NOTE: careful, if you clear the select it will pass null for selectedItem and your JS will explode for looking for .id of undefined. Either use optional chaining or as in my case set it conditionally to '' (empty string so formik works).

Step 5: Final code:

And so we are all set with a fully functional reusable Autocomplete dropdown select server-fetching async filtering, clearable thingy.

import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import AsyncSelect from 'react-select/async';


export default function AutocompleteComponent({
  fieldName,
  initialValue,
  setFieldValue,
  getOptionLabel,
  queryField,
}) {
  const [options, setOptions] = useState(null);
  const [initialObject, setInitialObject] = useState(null);

  // this function only finds the item from all the data that has the same id
  // that comes from the parent component (my case - formik initial)
  const findByValue = (fullData, specificValue) => {
    return fullData.find((e) => e.id === specificValue);
  };

  const loadData = async (query) => {
    // load your data using query HERE
    return res.data;
  };

  const getInitial = async () => {
     // load your data using query HERE
      const fetchedData = res.data;
      // match by id your initial value
      const initialItem = findByValue(fetchedData, initialValue); 
      // Set both initialItem and data options so component is initialized
      setInitialObject(initialItem);
      setOptions(fetchedData);
    }
  };

  // Hit this once in the beginning
  useEffect(() => {
    getInitial();
  }, []);

  return (
    <>
      {options !== null && initialObject !== null ? (
        <AsyncSelect
          isClearable
          getOptionLabel={getOptionLabel}
          getOptionValue={(option) => option.id}
          defaultValue={initialObject}
          defaultOptions={options}
          loadOptions={loadData}
          onChange={(selectedItem) => {
            const val = (selectedItem === null?'':selectedItem?.id);
            setFieldValue(fieldName, val)
          }}
        />
      ) : null}
    </>
  );
}

AutocompleteComponent.propTypes = {
  fieldName: PropTypes.string.isRequired,
  initialValue: PropTypes.string,
  setFieldValue: PropTypes.func.isRequired,
  getOptionLabel: PropTypes.func.isRequired,
  queryField: PropTypes.string.isRequired,
};

AutocompleteComponent.defaultProps = {
  initialValue: '',
};

I hope this saves you some time.

like image 139
mgPePe Avatar answered Nov 01 '25 12:11

mgPePe