在 2 个自动完成组件之间切换选定值时,MUI 自动完成不更新选定值
MUI Autocomplete not updating selected value when switching selected values between 2 autocomplete components
我有两个自动完成组件和一个元素,单击该元素可在两个自动完成组件之间切换值。当我这样做时,我也会切换选项。在自动完成组件中,我正在渲染一个带有 selected 值的隐藏字段。当我单击切换值的元素时,我可以在 DOM 中看到隐藏字段正在 2 个自动完成组件之间正确更新。单击下拉菜单时,我还可以看到正在切换的选项。但是 label/text 字段没有更新。保留原始标签,不会更新到新标签。
父组件:
import React, { useState, useEffect, Suspense } from 'react';
import classes from './AirForm.module.css';
const AirType = React.lazy(() => import('./Controls/Air/AirType'));
const Airport = React.lazy(() => import('./Controls/Air/Airport'));
const AirForm = props => {
const [airTypeSelected, setAirTypeSelected] = useState('RoundTrip');
const [fromSelected, setFromSelected] = useState();
const [fromOptions, setFromOptions] = useState([]);
const [toSelected, setToSelected] = useState();
const [toOptions, setToOptions] = useState([]);
//const [switchFromTo, setSwitchFromTo] = useState(true);
const switchFromToHandler = () => {
setFromOptions(toOptions);
setToOptions(fromOptions);
setFromSelected(toSelected);
setToSelected(fromSelected);
//setSwitchFromTo(!switchFromTo);
}
//useEffect(() => {
// setFromSelected(toSelected);
// setToSelected(fromSelected);
//}, [switchFromTo]);
return (
<Suspense>
<AirType airTypeSelected={airTypeSelected} setAirTypeSelected={setAirTypeSelected} />
<form action={props.data.AirUrl} method="post">
<div className={classes.destinations}>
<div>
<Airport data={props.data} name="DepartureAirport" label="From" options={fromOptions} setOptions={setFromOptions} optionSelected={fromSelected} setOptionSelected={setFromSelected} onSwitch={switchFromToHandler} includeSwitch={true} />
</div>
<div>
<Airport data={props.data} name="ArrivalAirport" label="To" options={toOptions} setOptions={setToOptions} optionSelected={toSelected} setOptionSelected={setToSelected} includeSwitch={false} />
</div>
</div>
</form>
</Suspense>
);
}
export default AirForm;
包含自动完成组件的组件:
import React, { useState, useMemo, useEffect, useRef } from 'react';
import Styles from '../../../../../Theming/Styles';
import classes from './Airport.module.css';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import CircularProgress from '@mui/material/CircularProgress';
import { debounce } from '@mui/material';
const Airport = props => {
const [inputValue, setInputValue] = useState('');
const [searching, setSearching] = useState(true);
const [loading, setLoading] = useState(false);
const abortConRef = useRef();
const fetchData = useMemo(() =>
debounce((request, callback) => {
if (!props.data.AirportLookupUrl) {
callback({ success: false });
return;
}
if (abortConRef.current) abortConRef.current.abort();
abortConRef.current = new AbortController();
fetch(props.data.AirportLookupUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ q: request }),
signal: abortConRef.current.signal
})
.then(response => response.json())
.then(result => {
callback(result);
})
.catch((error) => {
if (error.name !== 'AbortError') {
throw error;
}
});
}, 400),
[]
);
useEffect(() => {
if (inputValue === '') {
props.setOptions([]);
return undefined;
}
setLoading(true);
setSearching(true);
fetchData(inputValue, (result) => {
setLoading(false);
setSearching(false);
if (result.success) {
const newOptions = result.value.map(val => {
const label = `(${val.Code})${val.City && ' ' + val.City} - ${val.Name}`;
return {
label: label,
value: val.Code
};
});
props.setOptions(newOptions);
} else {
props.setOptions([]);
}
});
}, [inputValue, fetchData]);
return (
<>
<Autocomplete
disablePortal
options={props.options}
value={props.optionSelected}
autoComplete
autoHighlight={true}
includeInputInList
fullWidth
onChange={(event, value) => { props.setOptionSelected(value); }}
onInputChange={(event, value) => {
if (event.type === 'change') {
setInputValue(value);
}
}}
noOptionsText={searching ? 'Type to search' : 'No options'}
loading={loading}
sx={Styles}
renderInput={(params) => (
<>
<input type="hidden" name={props.name} value={props.optionSelected?.value} />
<TextField
{...params}
label={props.label}
InputProps={{
...params.InputProps,
endAdornment: (
<>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</>
)
}}
/>
</>
)}
/>
{props.includeSwitch && <div onClick={props.onSwitch} className={classes.switch}><i class="fas fa-sync" aria-hidden="true"></i></div>}
</>
);
};
export default Airport;
更新
这是一个codesandbox。不确定为什么它不在 Whosebug 中 运行ning,但如果您导航到沙箱,它会 运行。 https://codesandbox.io/s/strange-hertz-d8rtb8
如果您 select 任一自动完成组件中的选项然后单击切换按钮,您将看到选项已切换但 selected 选项不会更改。为每个自动完成呈现一个隐藏字段,当您单击切换按钮时,您可以看到隐藏字段的值已正确更新,但自动完成显示的文本值永远不会更新。
<iframe src="https://codesandbox.io/embed/strange-hertz-d8rtb8?fontsize=14&hidenavigation=1&theme=dark"
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
title="strange-hertz-d8rtb8"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
更新 2
问题是因为我没有为 selected 选项设置初始状态。设置状态已解决问题。
解法:
关于你的codesandbox的解决方案你需要2个步骤来解决你的问题;
- 在
App.js
内使选定的 useStates 初始值 null
const [fromSelected, setFromSelected] = useState(null);
const [toSelected, setToSelected] = useState(null);
- 在
onInputChange
event
上添加可选链接 MyAutocomplete
。由于输入更改发生在受控状态,这不是事件并且失败了。
onInputChange={(event, value) => {
if (event?.type === "change") {
setInputValue(value);
}
}}
建议:
我还可以在这里提出一些可能的改进建议;
- 使用多个钩子是危险的,因为它们中的每一个都重新呈现组件及其值。您可以像下面这样组合您的钩子;
const [options, setOptions] = useState({
from: {
selected: null,
options: fromList
},
to: {
selected: null,
options: toList
}
});
//
const handleSetOptions = (area, key, value) => {
setOptions({
...options,
[area]: {
...options[area],
[key]: value
}
});
};
//From Autocompolete Props, To is same just need to change from in here
options={options.from.options}
setOptions={(value) => handleSetOptions("from", "options", value)}
optionSelected={options.from.selected}
- 更新状态时可以使用previousState。它与在您的示例中使用状态相同,但它是异步和多挂钩情况的好习惯。
const switchFromToHandler = () => {
setOptions((prevOptions) => ({
from: prevOptions.to,
to: prevOptions.from
}));
};
您可以查看 codesandbox
的更新版本
我有两个自动完成组件和一个元素,单击该元素可在两个自动完成组件之间切换值。当我这样做时,我也会切换选项。在自动完成组件中,我正在渲染一个带有 selected 值的隐藏字段。当我单击切换值的元素时,我可以在 DOM 中看到隐藏字段正在 2 个自动完成组件之间正确更新。单击下拉菜单时,我还可以看到正在切换的选项。但是 label/text 字段没有更新。保留原始标签,不会更新到新标签。
父组件:
import React, { useState, useEffect, Suspense } from 'react';
import classes from './AirForm.module.css';
const AirType = React.lazy(() => import('./Controls/Air/AirType'));
const Airport = React.lazy(() => import('./Controls/Air/Airport'));
const AirForm = props => {
const [airTypeSelected, setAirTypeSelected] = useState('RoundTrip');
const [fromSelected, setFromSelected] = useState();
const [fromOptions, setFromOptions] = useState([]);
const [toSelected, setToSelected] = useState();
const [toOptions, setToOptions] = useState([]);
//const [switchFromTo, setSwitchFromTo] = useState(true);
const switchFromToHandler = () => {
setFromOptions(toOptions);
setToOptions(fromOptions);
setFromSelected(toSelected);
setToSelected(fromSelected);
//setSwitchFromTo(!switchFromTo);
}
//useEffect(() => {
// setFromSelected(toSelected);
// setToSelected(fromSelected);
//}, [switchFromTo]);
return (
<Suspense>
<AirType airTypeSelected={airTypeSelected} setAirTypeSelected={setAirTypeSelected} />
<form action={props.data.AirUrl} method="post">
<div className={classes.destinations}>
<div>
<Airport data={props.data} name="DepartureAirport" label="From" options={fromOptions} setOptions={setFromOptions} optionSelected={fromSelected} setOptionSelected={setFromSelected} onSwitch={switchFromToHandler} includeSwitch={true} />
</div>
<div>
<Airport data={props.data} name="ArrivalAirport" label="To" options={toOptions} setOptions={setToOptions} optionSelected={toSelected} setOptionSelected={setToSelected} includeSwitch={false} />
</div>
</div>
</form>
</Suspense>
);
}
export default AirForm;
包含自动完成组件的组件:
import React, { useState, useMemo, useEffect, useRef } from 'react';
import Styles from '../../../../../Theming/Styles';
import classes from './Airport.module.css';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import CircularProgress from '@mui/material/CircularProgress';
import { debounce } from '@mui/material';
const Airport = props => {
const [inputValue, setInputValue] = useState('');
const [searching, setSearching] = useState(true);
const [loading, setLoading] = useState(false);
const abortConRef = useRef();
const fetchData = useMemo(() =>
debounce((request, callback) => {
if (!props.data.AirportLookupUrl) {
callback({ success: false });
return;
}
if (abortConRef.current) abortConRef.current.abort();
abortConRef.current = new AbortController();
fetch(props.data.AirportLookupUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ q: request }),
signal: abortConRef.current.signal
})
.then(response => response.json())
.then(result => {
callback(result);
})
.catch((error) => {
if (error.name !== 'AbortError') {
throw error;
}
});
}, 400),
[]
);
useEffect(() => {
if (inputValue === '') {
props.setOptions([]);
return undefined;
}
setLoading(true);
setSearching(true);
fetchData(inputValue, (result) => {
setLoading(false);
setSearching(false);
if (result.success) {
const newOptions = result.value.map(val => {
const label = `(${val.Code})${val.City && ' ' + val.City} - ${val.Name}`;
return {
label: label,
value: val.Code
};
});
props.setOptions(newOptions);
} else {
props.setOptions([]);
}
});
}, [inputValue, fetchData]);
return (
<>
<Autocomplete
disablePortal
options={props.options}
value={props.optionSelected}
autoComplete
autoHighlight={true}
includeInputInList
fullWidth
onChange={(event, value) => { props.setOptionSelected(value); }}
onInputChange={(event, value) => {
if (event.type === 'change') {
setInputValue(value);
}
}}
noOptionsText={searching ? 'Type to search' : 'No options'}
loading={loading}
sx={Styles}
renderInput={(params) => (
<>
<input type="hidden" name={props.name} value={props.optionSelected?.value} />
<TextField
{...params}
label={props.label}
InputProps={{
...params.InputProps,
endAdornment: (
<>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</>
)
}}
/>
</>
)}
/>
{props.includeSwitch && <div onClick={props.onSwitch} className={classes.switch}><i class="fas fa-sync" aria-hidden="true"></i></div>}
</>
);
};
export default Airport;
更新
这是一个codesandbox。不确定为什么它不在 Whosebug 中 运行ning,但如果您导航到沙箱,它会 运行。 https://codesandbox.io/s/strange-hertz-d8rtb8 如果您 select 任一自动完成组件中的选项然后单击切换按钮,您将看到选项已切换但 selected 选项不会更改。为每个自动完成呈现一个隐藏字段,当您单击切换按钮时,您可以看到隐藏字段的值已正确更新,但自动完成显示的文本值永远不会更新。
<iframe src="https://codesandbox.io/embed/strange-hertz-d8rtb8?fontsize=14&hidenavigation=1&theme=dark"
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
title="strange-hertz-d8rtb8"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
更新 2
问题是因为我没有为 selected 选项设置初始状态。设置状态已解决问题。
解法:
关于你的codesandbox的解决方案你需要2个步骤来解决你的问题;
- 在
App.js
内使选定的 useStates 初始值
null
const [fromSelected, setFromSelected] = useState(null);
const [toSelected, setToSelected] = useState(null);
- 在
onInputChange
event
上添加可选链接MyAutocomplete
。由于输入更改发生在受控状态,这不是事件并且失败了。
onInputChange={(event, value) => {
if (event?.type === "change") {
setInputValue(value);
}
}}
建议:
我还可以在这里提出一些可能的改进建议;
- 使用多个钩子是危险的,因为它们中的每一个都重新呈现组件及其值。您可以像下面这样组合您的钩子;
const [options, setOptions] = useState({
from: {
selected: null,
options: fromList
},
to: {
selected: null,
options: toList
}
});
//
const handleSetOptions = (area, key, value) => {
setOptions({
...options,
[area]: {
...options[area],
[key]: value
}
});
};
//From Autocompolete Props, To is same just need to change from in here
options={options.from.options}
setOptions={(value) => handleSetOptions("from", "options", value)}
optionSelected={options.from.selected}
- 更新状态时可以使用previousState。它与在您的示例中使用状态相同,但它是异步和多挂钩情况的好习惯。
const switchFromToHandler = () => {
setOptions((prevOptions) => ({
from: prevOptions.to,
to: prevOptions.from
}));
};
您可以查看 codesandbox
的更新版本