react-select Creatable: transforming created options - reactjs

I trying to use react-select's Creatable select component to allow users to add multiple CORS Origins to be registered for my authentication server. I would like to be able to allow users to paste full URLs, and have these URLs be transformed into Origins (format: <protocol>://<origin>[:port]) once they are added to the Creatable select.
As an example, the user could paste http://some-domain.com:1234/management/clients?param1=abc&param2=123#fragment_stuff into the Creatable select, and this whole URL would automatically be converted/added as just its origin components: http://some-domain.com:1234.
This is a reduced version the component I've wrote (TypeScript):
import CreatableSelect from 'react-select/creatable';
...
type MyOptionType = {
label: string,
value: string,
}
function SomeComponent(props:{}) {
const [options, setOptions] = useState<MyOptionType[]>([]);
const onOptionsChanged = (newOptions: OptionsType<MyOptionType>) => {
// Get only options containing valid URLs, with valid origins
const validUrlsWithOrigins = newOptions.filter(option => {
try {
return !!(new URL(option.value).origin);
} catch (error) {
return false;
}
});
// Transform options (e.g.: "http://some-domain.com:1234/abc?def=ghi#jkl" will become "http://some-domain.com:1234")
const newOptionsOrigins = validUrlsWithOrigins
.map(option => new URL(option.value).origin)
.map(origin => ({label: origin, value: origin}));
setOptions(newOptionsOrigins);
}
return <CreatableSelect isMulti options={options} onChange={onOptionsChanged} />
}
While debugging using React Developer Tools, I can see that the state of my component is being transformed accordingly, having only the origin part of my URLs being kept in the state:
The problem is that the Creatable select component is rendering the full URL instead of only the URL's Origin:
Why isn't the Creatable select in sync with the component's state? Is there a way to solve this, or is it a limitation on react-select?

You need to distinguish two things here - options prop of CreatableSelect holds an array of all the possibilites. But the value of this component is managed by value property.
You can check Multi-select text input example on docs page but basically you'll need to:
keep values and option separetly:
const [options, setOptions] = React.useState<MyOptionType[]>([]);
const [value, setValue] = React.useState<MyOptionType[]>([]);
const createOption = (label: string) => ({
label,
value: label
});
<CreatableSelect
isMulti
options={options}
value={options}
onChange={onOptionsChanged}
/>
and modify your onOptionsChanged function
set value of transformed and validated input
add new options to options state variable (all options, without duplicates)
Here's some example:
// Transform options (e.g.: "http://some-domain.com:1234/abc?def=ghi#jkl" will become "http://some-domain.com:1234")
const newOptionsOrigins = validUrlsWithOrigins
.map((option) => new URL(option.value).origin)
.map((origin) => createOption(origin));
setValue(newOptionsOrigins);
//get all options without duplicates
const allUniqueOptions: object = {};
[...newOptionsOrigins, ...options].forEach((option) => {
allUniqueOptions[option.value] = option.value;
});
setOptions(
Object.keys(allUniqueOptions).map((option) => createOption(option))
);
};

Related

How do I parse the HTML from the Lexical editorState without an extra Lexical editor

I have a posts system and when someone submits a post I am saving the editorState as a JSON string in a Postgres database.
Then, when I need to show the HTML, I am using a custom hook which loads an auxiliary editor just to obtain the HTML.
Is this the proper way? Looks to me a bit overengineering 🤔
I don't want to load other editor, and render it too just to obtain the HTML.
Any idea of how to improve this or suggestion for a different approach? 😄
The render post component:
export const PostDetails = () => {
const postSlug = useParam("postSlug", "string")
const [postDetails] = useQuery(getPostPageDetails, { slug: postSlug })
const { html, AuxEditor } = useGetHtmlFromState(postDetails.content as unknown as EditorState)
return (
<>
<AuxEditor />
<Text dangerouslySetInnerHTML={{ __html: html }} />
</>
)
}
The hook to get the HTML useGetHtmlFromState (it uses the same config as the input)
export const useGetHtmlFromState = (state: EditorState) => {
const [html, setHtml] = useState("")
function MyCustomStateToHtmlPlugin() {
const [editor] = useLexicalComposerContext()
editor.update(() => {
const html = $generateHtmlFromNodes(editor, null)
setHtml(html)
})
return null
}
const AuxEditor = () => {
return (
<LexicalComposer
initialConfig={{
namespace: "MyEditor",
onError: console.error,
editorState: state,
theme: exampleTheme,
nodes: [
HeadingNode,
ListNode,
ListItemNode,
QuoteNode,
CodeNode,
CodeHighlightNode,
TableNode,
TableCellNode,
TableRowNode,
AutoLinkNode,
LinkNode,
],
}}
>
<MyCustomStateToHtmlPlugin />
</LexicalComposer>
)
}
return { html, AuxEditor }
}
I think what you have is a creative way to turn SerializedEditorState into HTML. You could experiment with #lexical/headless, but it's essentially the same approach. Alternatively, there are a couple of other ways to solve the underlying problem of displaying saved state.
(1) You can generate the HTML up front and save it to the DB alongside the lexical state. It's a bit duplicative, but it works for the "write once, read many times" use case you've described.
(2) You can use a read-only lexical editor to display the saved state instead of converting it into HTML. You can configure the editor with a different theme if you need more control over the styling of specific elements.
I hope that helps!

How to access input value inside `transformErrors`

When mapping errors inside transformErrors callback, I need to know the actual value of the input in question.
I need this to create a system for composing multiple existing formats into new composite formats. I want to match the input value against each of the "basic" formats and display the error for the one that fails. The allOf method of composing formats unfortunately doesn't work for me, for reasons very specific to my project.
I tried injecting the form data into my tranformErrors callback via currying and reading the data directly:
import _ from 'lodash'
import Form from '#rjsf/core'
const makeTransformErrors = formData => errors => {
errors.forEach(error => {
if (error.name === 'format') {
const value = _.get(formData, error.property)
// ...
}
})
}
const WrapedForm = (formData, ...rest) => {
const transformErrors = makeTransformErrors(formData)
return (
<Form
transformErrors={transformErrors}
formData={formData}
{...rest}
/>
)
}
but this way value lags one keystroke behind the actual state of the form, which is what I was expecting. Unfortunately this doesn't work even when I don't pass formData into makeTransformErrors directly, but instead I pass in an object containing formData and directly mutate it imeditately inside Forms onChange, which I was expecting to work.
What are other possible ways of accessing the field's value? Maybe it could be possible to configure (or patch) ajv validator to attatch the value to validation error's params?
Not sure exactly what kind of error validation you are trying todo, but have you tried using validate?
It can be passed as such :
<Form .... validate={validate} />
where validate is a function that takes as arguments formData and errors.
See documentation here
Ok, I found a way of achieving what I want, but it's so hacky I don't think I want to use it. I can get the up-to-date value when combining the above mentioned prop mutation trick with using a getter for the message, postponing the evaluation until the message is actually read, which happens to be enough:
import _ from 'lodash'
import Form from '#rjsf/core'
const makeTransformErrors = formDataRef => errors => {
return errors.map(error => {
if (error.name !== 'format') return error
return {
...error,
get message() {
const value = _.get(propPath, formDataRef.current) // WORKS! But at what cost...
}
}
})
}
const WrapedForm = (formData, onChange, ...rest) => {
const formDataRef = React.useRef(formData)
const transformErrors = makeTransformErrors(formDataRef)
handleChange = (params) => {
formDataRef.current = params.formData
onChange(params)
}
return (
<Form
transformErrors={transformErrors}
onChange={handleChange}
formData={formData}
{...rest}
/>
)
}

Populating a Material-UI dropdown with Redux store data

I am trying to get my Redux store fields to automatically populate a method I have imported. Am I going about this the right way in order to get this done? Do I need to create a mapping options for each field?
I have each of my dropdowns inserted with a PopulateDropdown list and the fields in each of them but I need them split as per the id and text.
Am I accessing my redux store correctly below? I have the array declared on up my function component by using const fields = useSelector(state => state.fields);
Update
I have the method inserted into where the dropdowns should be however I don't think I am accessing the data correctly which is causing the problem. The fields array has been de-structured into the six different fields for each dropdown and different mappingOptions have been created for each one.
What do I need to do to get the data into the method? the examples I have seen have static arrays declared on the component rather than use the Redux store.
const fields = useSelector(state => state.fields);
// can destructure individual fields
const { diveSchoolList, currentList, regionList, diveTypeList, visibilityList, diveSpotList } = fields;
populateDropdown method that I have imported
export const PopulateDropdown = ({ dataList = [], mappingOptions, name, label }) => {
const { title, value } = mappingOptions;
return (
<FormControl style={{ width: 200 }} >
<InputLabel id={label}>{label}</InputLabel>
<Select labelId={label} name={name} >
{dataList.map((item) => (
<MenuItem value={item[value]}>{item[title]}</MenuItem>
))}
</Select>
</FormControl>
);
};
imported dropdown menu
<PopulateDropdown
dataList={diveType}
mappingOptions={mappingOptions}
name="fieldName"
label="Select dive type"
value={dive.typeID}
onChange={handleChange}/>
Update
I have updated my action, reducer and populateFields method however I am still having trouble mapping the redux data to my two property fields. In the Redux tree the fields should be under the fields.data.fieldlists as they print when I console log them.
What way should I be populating them into the titleProperty etc? It is currently looking like it might be populating but a large box drops downs that I can't see any values inside.
// select user object from redux
const user = useSelector(state => state.user);
// get the object with all the fields
const fields = useSelector(state => state.fields);
// can destructure individual fields
const { diveSchoolList = [],
currentList = [],
regionList = [],
diveTypeList = [],
visibilityList = [],
diveSpotList = [],
marineTypeList = [],
articleTypeList = []
} = fields;
.........
<PopulateDropdown
dataList={fields.data.diveTypeList} // the options array
titleProperty={fields.data.diveTypeList.diveTypeID} // option label property
valueProperty={fields.data.diveTypeList.diveType} // option value property
label="Dive Type Name" // label above the select
placeholder="Select dive type" // text show when empty
value={dive.typeID} // get value from state
onChange={handleChange(setDive.typeID)} // update state on change
/>
Your PopulateDropdown component looks correct except that we need it to use the value and onChange that we passed down as props.
My personal preference would be to use separate properties valueProperty and titleProperty instead of passing a single mappingOptions. That way you don't need to create objects for every dropdown, you just set the two properties in your JSX. You could get rid of this part entirely if you normalized your data such that the elements of every list have the same properties id and label.
<PopulateDropdown
dataList={diveTypeList} // the options array
titleProperty={"diveTypeId"} // option label property
valueProperty={"diveType"} // option value property
label="Dive Type Name" // label above the select
placeholder="Select dive type" // text show when empty
value={dive.typeID} // get value from state
onChange={handleChange("typeId")} // update state on change
/>
export const PopulateDropdown = ({
dataList = [],
valueProperty,
titleProperty,
label,
...rest // can just pass through all other props to the Select
}: Props) => {
return (
<FormControl style={{ width: 200 }}>
<InputLabel id={label}>{label}</InputLabel>
<Select {...rest} labelId={label}>
{dataList.map((item) => (
<MenuItem value={item[valueProperty]}>{item[titleProperty]}</MenuItem>
))}
</Select>
</FormControl>
);
};
It looks like the ids in currentId are actually a number, so at some point in your code you will want to convert that with parseInt because e.target.value is always a string, though maybe the backend can handle that.
Loading the API Data
It looks like you figured out how to fetch all of the fields in one API call which is great. You are saving it to a property fields on the fields reducer which creates the structure state.fields.fields. Since you are replacing the whole state, you can just return the whole thing as the entire slice state.
You can initialize your state object with empty arrays, or you can use an empty object {} as your initial state and fallback to an empty array when you destructure the arrays off of it, like const {diveSchoolList = [], currentList = []} = fields.
export const requireFieldData = createAsyncThunk(
"fields/requireData", // action name
// don't need any argument because we are now fetching all fields
async () => {
const response = await diveLogFields();
return response.data;
},
// only fetch when needed: https://redux-toolkit.js.org/api/createAsyncThunk#canceling-before-execution
{
// _ denotes variables that aren't used - the first argument is the args of the action creator
condition: (_, { getState }) => {
const { fields } = getState(); // returns redux state
// check if there is already data by looking at the didLoadData property
if (fields.didLoadData) {
// return false to cancel execution
return false;
}
}
}
);
const fieldsSlice = createSlice({
name: "fields",
initialState: {
currentList: [],
regionList: [],
diveTypeList: [],
visibilityList: [],
diveSpotList: [],
diveSchoolList: [],
marineTypeList: [],
articleTypeList: [],
didLoadData: false,
},
reducers: {},
extraReducers: {
// picks up the pending action from the thunk
[requireFieldData.pending.type]: (state) => {
// set didLoadData to prevent unnecessary re-fetching
state.didLoadData = true;
},
// picks up the success action from the thunk
[requireFieldData.fulfilled.type]: (state, action) => {
// want to replace all lists, there are multiple ways to do this
// I am returning a new state which overrides any properties
return {
...state,
...action.payload
}
}
}
});
So in the component we now only need to call one action instead of looping through the fields.
useEffect(() => {
dispatch(requireFieldData());
}, []);

Material UI - Autocomplete with React Hook Form

I am building a form using Material UI's Autocomplete feature and using recoil for state management. I am also using react-hook-form. I am needing the following criteria met:
Need typeahead that allows first letter typed to return a list of options from an API to show in Autocomplete. Each letter will return a different list of options to select from.
Need to also allow freeSolo so that user can enter a value manually
Needs to be required and follow a pattern to validate the form.
I am using react-hook-form's <Controller> to control the input and allow for features like validation, displaying error messages in the helper text, etc.
The Problems: I am having issues with the typeahead filtering out options based on what I type, along with allowing freeSolo. As I type a new value, the list of options do not filter. The popup just stays open. I also need to validate on change of input for the pattern validation. I have tried with the following example with onInputChange to make use of react-hook-form's useForm and setValue to manually set the value of the field and validate the form. ({shouldValidate: true}). The below example is a custom, reusable component I created for Autocomplete, as well as using that custom component in other parent components. I hope I included as much details as possilbe, but if not, please let me know if you need anything more. Any assistance would be very appreciative!
Parent Component:
const setTrainType = useSetRecoilState(TrainType)
// Trains would return a list of trains based on letter of train type that was passed from input
const trainsList = useRecoilValue(Trains)
const trainOptions = useMemo(() => trainsList.map(trainIDFormatted), [
trainsList,
])
const handleInputChange = useCallback(
(_e: unknown, option: string, reason: string) => {
const capitalized =
option === capitalize(option) ? option : capitalize(option)
setValue('trainID', capitalized, {shouldValidate: true})
if (['input', 'reset'].includes(reason) && capitalized !== '') {
setTrainType(capitalized.charAt(0))
} else {
setTrainType(undefined)
}
},
[setTrainType, setValue],
)
<Autocomplete
autoSelect
freeSolo
disabled={disabled}
helperText=" "
label="Select a train"
name="trainID"
options={trainOptions}
rules={{
pattern: {
message: 'Must match train ID pattern',
value: /^(?:[A-Z]-?[A-Z ]{6}-?[0-9 ]-?[0-9 ]{2}[A-Z ])?$/,
},
required: 'Train is required',
}}
onInputChange={handleInputChange}
/>
Custom autocomplete component:
import {
AutocompleteProps,
Autocomplete as MuiAutocomplete,
} from '#material-ui/lab'
import {get} from 'lodash'
import React, {ReactNode, useCallback} from 'react'
import {
Controller,
ControllerProps,
FieldError,
useFormContext,
} from 'react-hook-form'
import {useRenderInput} from './hooks'
interface Props
extends Pick<ControllerProps<'select'>, 'rules'>,
Omit<
AutocompleteProps<string, false, false, true>,
'error' | 'onChange' | 'required' | 'renderInput'
> {
helperText?: ReactNode
label?: string
name: string
}
/**
* Render controlled autocomplete. Use react-form-hook's FormProvider.
* #param props Component properties
* #param props.helperText Default helper text for error
* #param props.label Input label
* #param props.name Name identifier for react-hook-form
* #param props.required If true then item is required
* #param props.rules Select rules
* #return React component
*/
export const Autocomplete = ({
helperText,
label,
name,
rules,
...props
}: Props) => {
// eslint-disable-next-line #typescript-eslint/unbound-method
const {control, errors, watch} = useFormContext()
const error: FieldError | undefined = get(errors, name)
const required = get(rules, 'required') !== undefined
const value = watch(name)
const renderAutocompleteInput = useRenderInput({
error: error !== undefined,
helperText: get(error, 'message', helperText),
label,
required,
})
const handleOnChange = useCallback(
(_e: unknown, option: string | null) => option,
[],
)
const renderAutocomplete = useCallback(
params => (
<MuiAutocomplete
{...props}
{...params}
renderInput={renderAutocompleteInput}
onChange={handleOnChange}
/>
),
[handleOnChange, props, renderAutocompleteInput],
)
return (
<Controller
control={control}
defaultValue={value ?? ''}
name={name}
render={renderAutocomplete}
rules={rules}
/>
)
}
What it looks like:

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?
react-select has several examples in the documentation including an entire section dedicated to AsyncSelect which include inline code examples with codesandbox links.
It's worth noting that there are three unique props specific to the AsyncSelect
loadOptions
defaultOptions
cacheOptions
The primary difference between AsyncSelect and Select is that a Select is reliant on an options prop (an array of options) whereas the AsyncSelect is instead reliant on a loadOptions prop (an async function which provides a callback to set the options from an api).
Often api autocomplete lookups filter results on the server so the callback on the loadOptions does not make assumptions on filtering the results returned which is why they may need to be filtered client-side prior to passing them to the AsyncSelect state.
Here is a simple code example.
import React from 'react';
import AsyncSelect from 'react-select/async';
const filterOptions = (options, inputValue) => {
const candidate = inputValue.toLowerCase();
return options.filter(({ label }) => label.toLowerCase().includes(candidate);
};
const loadOptions = (inputValue, callback) => {
const url = `www.your-api.com/?inputValue=${inputValue}`;
fetch(url).then(resp => {
const toSelectOption = ({ id, name }) => ({ label: name, value: id });
// map server data to options
const asyncOptions = resp.results.map(toSelectOption);
// Filter options if needed
const filtered = filterOptions(asyncOptions, inputValue);
// Call callback with mapped and filtered options
callback(filtered);
})
};
const AsyncLookup = props => (
<AsyncSelect
cacheOptions
loadOptions={loadOptions}
defaultOptions
{...props}
/>
);
export default AsyncLookup
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.
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:
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:
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:
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.

Resources