I have build this generic dropdown/select component with an async function to get datasets. For some reason I get the message The 'fetchData' function makes the dependencies of useEffect Hook (at line 48) change on every render. To fix this, wrap the definition of 'fetchData' in its own useCallback() Hook.eslintreact-hooks/exhaustive-deps.
I don't change any value of a dependency from my useEffect hook since these properties are controlled by my redux slice....
Select component:
import React, { useEffect } from 'react';
import { Form, Select, Typography } from 'antd';
import PropTypes from 'prop-types';
import styled from 'styled-components';
const StyledSelect = styled(Select)`
&.ant-select-loading .ant-select-selection-item {
display: none;
}
`;
const { Text } = Typography;
const CustomSelect = ({
endpointKey,
dataKey,
customLabel,
required = false,
dataSet,
fetchDataSet,
disabled = false,
fullOptionHeight = false,
onChange = null,
showSearch = false,
setLoading = null,
}) => {
const fetchData = async (searchText) => {
if (setLoading) {
setLoading(true);
}
await fetchDataSet({ endpointKey, searchText });
if (setLoading) {
setLoading(false);
}
};
useEffect(() => {
const dataSetPresent = !!dataSet.data && !!Object.keys(dataSet.data).length;
const hasError = dataSet.errorMessage && dataSet.errorMessage.length;
if (
!dataSetPresent &&
dataSet.loadingStatus !== 'loading' &&
dataSet.loadingStatus !== 'loaded' &&
!hasError
) {
fetchData();
}
}, [fetchData, dataSet]);
const { loadingStatus, data, errorMessage } = dataSet;
const label = customLabel || endpointKey;
const formErrorMessage = 'Please select ' + label.toLowerCase();
const placeholder = `-- Select a ${label.toLowerCase()} --`;
const renderSelect = () => {
if (errorMessage !== '') {
return <Text type="danger">{errorMessage}</Text>;
}
return (
<StyledSelect
disabled={loadingStatus === 'loading' || disabled}
loading={loadingStatus === 'loading'}
placeholder={placeholder}
optionFilterProp="children"
style={{ maxWidth: '500px' }}
size="large"
onChange={onChange}
showSearch={showSearch}
onSearch={(value) => {
if (showSearch) {
fetchData(value);
}
}}
>
{Object.keys(data).map((dataObject) => {
return (
<Select.Option
className={`${fullOptionHeight ? 'full-option-height' : ''}`}
value={dataObject}
key={dataObject}
>
{data[dataObject]}
</Select.Option>
);
})}
</StyledSelect>
);
};
return (
<Form.Item
label={label}
name={dataKey}
rules={[{ required, message: formErrorMessage }]}
>
{renderSelect()}
</Form.Item>
);
};
CustomSelect.propTypes = {
endpointKey: PropTypes.string.isRequired,
dataKey: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
customLabel: PropTypes.string,
required: PropTypes.bool,
fetchDataSet: PropTypes.func,
showSearch: PropTypes.bool,
dataSet: PropTypes.object,
disabled: PropTypes.bool,
fullOptionHeight: PropTypes.bool,
onChange: PropTypes.func,
setLoading: PropTypes.func,
};
export default CustomSelect;
React Slice with the async hook and state change handling:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import callApi from 'utils/api';
export const SELECT_KEY = 'select';
export const fetchDataSet = createAsyncThunk(
'select/request-data',
async ({ endpointKey, searchText }) => {
const endpoint = `data/${endpointKey}`;
try {
const { data } = await callApi({
endpoint,
});
return data;
} catch (error) {
console.error('ERROR', error);
throw error;
}
}
);
export const selectSlice = createSlice({
name: SELECT_KEY,
initialState: {},
reducers: {},
extraReducers: {
[fetchDataSet.pending]: (state, action) => {
const key = action.meta.arg.endpointKey;
return {
...state,
[key]: {
loadingStatus: 'loading',
errorMessage: '',
data: {},
},
};
},
[fetchDataSet.fulfilled]: (state, action) => {
const key = action.meta.arg.endpointKey;
return {
...state,
[key]: {
loadingStatus: 'loaded',
errorMessage: '',
data: action.payload,
},
};
},
[fetchDataSet.rejected]: (state, action) => {
const key = action.meta.arg.endpointKey;
return {
...state,
[key]: {
loadingStatus: 'error',
errorMessage: action.error.message,
data: {},
},
};
},
},
});
export const selectReducer = selectSlice.reducer;
export const dataSetSelector = (state, key) => state.select[key] || {};