How to use react-select-async-paginate with Formik

by admin
react-select-async-paginate Formik

I am currently tasked with building a React component that is able to use/populate a dropdown with hundred of thousands of options. Of course we don’t want to retrieve all records at once from the server but only serve what the user is searching for.

I have created a reusable component, hooked into Formik (Link), that you can use to get paginated dropdown data from your API. I have used the ‘react-select-async-paginate’ package, find more information here.

Our API (a GET) returns this JSON:

{
  "messages": [
    "string"
  ],
  "succeeded": true,
  "data": {
    "items": [
      {
        "value": 0,
        "label": "string"
      }
    ],
    "totalCount": 0
  }
}

And has the following input properties:

Keyword
SkipCount
MaxResultCount

Keyword is used for searching

SkipCount used to skip X amount of records on the server

MaxResultCount is to specify how many records you want to return at once from the API. We use a default of 30.

We have built the following React Component:

import { InputLabel, FormHelperText } from '@mui/material'
import FormControl from '@mui/material/FormControl'
import { FormikContextType, useFormikContext } from 'formik'
import { AsyncPaginate } from 'react-select-async-paginate'
import { ReactNode } from 'react'
import * as _ from 'lodash'
import { DropdownValue } from 'src/types/common'

interface AppSelectInputProps {
  name: string
  selectedValuesKey?: string
  label?: string
  isMultiple?: boolean
  getdata?: any
  [x: string]: any
}

const AppSelectInput: React.FC<AppSelectInputProps> = ({
  name,
  selectedValuesKey,
  label,
  getdata = null,
  isMultiple = false,
  ...otherProps
}) => {
  const { setFieldTouched, setFieldValue, values, errors, touched }: FormikContextType<any> = useFormikContext()

  const onChange = (event: any) => {
    if (isMultiple) {
      setFieldValue(
        name,
        event.map(function (item: any) {
          return item.value
        })
      )

      if (selectedValuesKey) {
        setFieldValue(selectedValuesKey, event.option)
      }
    } else {
      setFieldValue(name, event.value)

      if (selectedValuesKey) {
        setFieldValue(selectedValuesKey, event)
      }
    }
  }

  const loadPageOptions = async (q: any, prevOptions: any, { take, skip }: any) => {
    const data = await getdata({
      keyword: q || '',
      maxResultCount: take,
      skipCount: skip
    })
      .unwrap()
      .then((res: any) => res.data)

    return {
      options: data.items.map((item: DropdownValue) => {
        return {
          value: item.value,
          label: item.name || item.label
        }
      }),
      hasMore: skip < data.totalCount,
      additional: {
        skip: skip + 30,
        take: take
      }
    }
  }

  const defaultAdditional = {
    skip: 0,
    take: 30
  }

  return (
    <div>
      {label && <InputLabel sx={{ fontSize: 14 }}>{label}</InputLabel>}

      <FormControl fullWidth error={(_.get(errors, name) && _.get(touched, name)) as boolean}>
        <AsyncPaginate
          className='react-select'
          placeholder='Selecteer...'
          isMulti={isMultiple}
          onChange={onChange}
          value={_.get(values, selectedValuesKey || '')}
          loadOptions={loadPageOptions as any}
          onBlur={() => setFieldTouched(name)}
          additional={defaultAdditional}
          {...otherProps}
        />
      </FormControl>
      {_.get(touched, name) && _.get(errors, name) && (
        <FormHelperText sx={{ fontSize: 14 }} error={true}>
          {_.get(errors, name) as ReactNode}
        </FormHelperText>
      )}
    </div>
  )
}

export default AppSelectInput

And use it like this inside your Formik component:

<AppSelectInput
    getdata={getRegistrationStatuses as any}
    selectedValuesKey='registrationStatus'
    name='statusId'
    isMultiple={false}
/>

Our Initialvalues object looks like this:

const initialValues = {
   statusId: 0,
   registrationStatus: { value: 1, label: 'This is a test!' }
}

So, whenever we want to render selected values in the dropdown, we use the ‘selectedValuesKey’ specified when rendering the AppSelectInput component. In our example above that is the registrationStatus key. This is awesome when you want to create a ‘Edit’ form and load the previously selected value or values.

Our API call to the endpoint to render data is done with RTK-Query and is quite simple:

getRegistrationStatusesForDropdown: builder.query<Dropdown, BaseDropdownSearchParams>({
        query: params => ({
          url: '/v1/RegistrationStatus/Dropdown',
          method: 'GET',
          params: params
        }),
        providesTags: ['Status']
      }),

The interface Dropdown and BaseDropDownSearchParams look like this:

export interface BaseDropdownSearchParams {
  keyword: string
  skipCount: number
  maxResultCount: number
}

export interface Dropdown {
  messages: string[]
  succeeded: boolean
  data: {
    items: {
      label: string
      value: string
    }[]
  }
}

And thats it, a reusable component for server sided dropdowns!

Related Posts

Leave a Comment