I’m currently struggling updating the state of a complex interface with an action from a child component. Here are the main points:
- I have a child component
autocomplete.component.tsx
that is basically a field with items suggested below - I have a
form.tsx
component where I’m callingautocomplete.component.tsx
and I’m displaying the field with some fake data and with a dropdown - I can see the state being updated when I’m typing in the field correctly and the complex object (interface) showing it updated properly.
- When I’m clicking on the suggested item in the
autocomplete.component.tsx
you can see in the console from the codesandbox devbox showing the correct selected value, but the state was not updated to the complex object. I know state is async but is not updating when submitting the form (you can seehandleSubmit()
method) and Save button is not updated.
Devbox with “working” example (hope the link works)
Devbox example
I’m using ReactJS + Vite + Typescript
import IMovimiento from "../IMovimiento";
import { ChangeEvent, FormEvent, useState, useEffect } from "react";
import { movimientoDefaultState } from "../movimientoDefaultState";
import { SingleValue } from "react-select";
import Select from "react-select";
import IOption from "../IOption";
import AutocompleteCustom from "./autocomplete.component";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import ILugar from "../ILugar";
const GastoForm = () => {
let ioptionLugar = { value: "", label: "" } as SingleValue<IOption>;
const [isOpen, setIsOpen] = useState(false);
const [lugares, setLugares] = useState(Array<ILugar>);
const [suggestions, setSuggestions] = useState<string[]>();
//Stuff to declare
const [formFields, setFormFields] = useState<IMovimiento>(
movimientoDefaultState
);
const [, setLugar] = useState<SingleValue<IOption> | null>(null);
//Methods
const handleFieldChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormFields({ ...formFields, [name]: value });
};
//When dropdown changes
const handleDropDownChange = (
selectedOption: SingleValue<IOption>,
name: string
): void => {
const { value, label } = { ...selectedOption };
setFormFields({ ...formFields, [name]: value });
const ioption = { value: value, label: label };
console.log(formFields);
if (name === "LugarId") {
setLugar(selectedOption);
ioptionLugar = ioption;
}
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log(formFields);
return;
};
/*AUTOCOMPLETE*/
const changeHandlerAutocomplete = async (
e: ChangeEvent<HTMLInputElement>
) => {
const { name, value } = e.target;
//You can see here the state is being updated properly everytime I type...
setFormFields({ ...formFields, [name]: value });
if (value !== "" && value.length <= 2) return;
//Now we search for a possible match
if (value === "") {
setIsOpen(false);
setSuggestions([]);
return;
}
//Faking results from a source...
const response = ["Test", "Test2", "Test3"];
if (response) {
setSuggestions(response);
setIsOpen(true);
return;
}
setIsOpen(false);
setSuggestions([]);
};
//Method to try and grab the option from the list of autocomplete
const selectedItem = (e: React.MouseEvent<HTMLDivElement>) => {
const descripcion = e.currentTarget.getAttribute("data-item");
//Fake variable that I'm using to simulate another functionality, for the sake of the example is like this
const fakeLugar = 2;
//const ids = e.currentTarget.getAttribute("data-ids");
const selectedOptionInAutocomplete =
descripcion !== null ? descripcion.split("-")[0].trim() : "";
console.log(selectedOptionInAutocomplete);
/*I EXPECT HERE FOR DESCRIPCION TO BE ONE OF THE AUTOCOMPLETE OPTIONS,
NOT THE TEXT I'M WRITING.
ALSO I EXPECT FOR THE TEXTFIELD TO HAVE THE AUTOCOMPLETE TEXT SINCE I'M
TELLING THE STATE TO BE MY OPTION CHOSEN
*/
setFormFields({ ...formFields, Descripcion: selectedOptionInAutocomplete });
setFormFields({
...formFields,
["Descripcion"]: selectedOptionInAutocomplete,
});
//Handle the dropdown changes to update state, faking place 2 always to see state
const selectedLugar = lugares.find((i) => i.LugarId === fakeLugar);
ioptionLugar = {
value: selectedLugar?.LugarId.toString(),
label: selectedLugar?.Descripcion,
};
handleDropDownChange(ioptionLugar, "LugarId");
setIsOpen(false);
};
/*AUTOCOMPLETE*/
const mapperLugar = (): IOption[] => {
return lugares.map((item) => {
return { value: item.LugarId, label: item.Descripcion };
}) as unknown as IOption[];
};
useEffect(() => {
const getDropDowns = async () => {
const responseLugar = [
{ LugarId: 1, Descripcion: "Place 1" },
{ LugarId: 2, Descripcion: "Place 2" },
] as ILugar[];
setLugares(responseLugar);
};
getDropDowns();
}, []);
return (
<>
<Form onSubmit={handleSubmit}>
<Row className="mb-3">
<Col md={6}>
<AutocompleteCustom
name="Descripcion"
value={formFields.Descripcion}
isOpen={isOpen}
items={suggestions}
changeHandler={changeHandlerAutocomplete}
selectedItemHandler={(
e: React.MouseEvent<HTMLDivElement, MouseEvent>
) => selectedItem(e)}
>
{formFields.Descripcion}
</AutocompleteCustom>
</Col>
<Col md={6}>
<Select
options={mapperLugar()}
onChange={(selectedOption: SingleValue<IOption>) =>
handleDropDownChange(selectedOption, "LugarId")
}
value={ioptionLugar}
theme={(theme) => ({
...theme,
colors: {
...theme.colors,
primary: "#01A3FF",
primary25: "#01A3FF",
neutral0: "#1e1e25",
},
borderRadius: 4,
})}
/>
</Col>
</Row>
<Button variant="primary" type="submit">
Save
</Button>
</Form>
</>
);
};
export default GastoForm;
autocomplete.component.tsx
import { ChangeEvent, PropsWithChildren } from "react";
import Form from "react-bootstrap/Form";
interface AutocompleteCustomProps {
children: string;
name: string;
changeHandler?: (event: ChangeEvent<HTMLInputElement>) => void;
clickHandler?: (event: React.MouseEvent<HTMLInputElement>) => void;
value: string | number | undefined;
items: string[] | undefined;
isOpen: boolean;
selectedItemHandler: (e: React.MouseEvent<HTMLDivElement>) => void;
}
const AutocompleteCustom = ({
children,
name,
value,
changeHandler,
clickHandler,
items,
isOpen,
selectedItemHandler,
}: PropsWithChildren<AutocompleteCustomProps>) => {
return (
<>
<p>{children}</p>
<div className="easy-autocomplete">
<Form.Control
name={name}
placeholder={children}
type="text"
onChange={changeHandler}
value={value}
onClick={clickHandler}
autoComplete="off"
/>
{items && isOpen && (
<div className="easy-autocomplete-container">
<ul
className=""
style={isOpen ? { display: "block" } : { display: "none" }}
>
{items.map((item, i) => {
const descripcionValues = item.split(";");
if (descripcionValues.length === 0) {
return (
<li key={i}>
<div
className="eac-item"
data-item={item}
onClick={(e) => selectedItemHandler(e)}
>
{item}
</div>
</li>
);
}
//It's a complex item, make it different
const description = descripcionValues[0];
const values = descripcionValues[1];
return (
<li key={i}>
<div
className="eac-item"
data-item={description}
data-ids={values}
onClick={(e) => selectedItemHandler(e)}
>
{description}
</div>
</li>
);
})}
</ul>
</div>
)}
</div>
</>
);
};
export default AutocompleteCustom;
App.tsx
import "./App.css";
import GastoForm from "./components/form";
function App() {
return (
<>
<GastoForm />
</>
);
}
export default App;
IMovimiento.ts – Descripcion is the field I want updated after option is selected
export default interface IMovimiento {
MovimientoId: number;
Descripcion: string;
Monto: number;
Moneda: string;
Fecha: Date;
Ticket: string;
Estatus: string;
Transferencia: string | null;
Observacion: string | undefined;
CuentaId: number;
Cuenta: string;
TipoMovimientoId: number;
TipoMovimiento: string;
CategoriaId: number;
Categoria: string;
SubCategoriaId: number;
SubCategoria: string;
CompradorId: number;
Comprador: string;
LugarId: number;
NombreLugar: string;
Etiquetas: (string | number | null | undefined)[];
Mes: string;
MesSuscripcion: string;
AnioSuscripcion: string;
ServicioId: number;
CuentaIdCredito: number;
FechaPago: Date | null;
}