I have a DatePicker which is throwing the following error message: Uncaught TypeError: Cannot read properties of undefined (reading 'value').
My data is being pulled from an API where some items date field is null. Initial render of the page is fine, where the TextFields and 2 empty date pickers display, but when I input a date, the error is thrown.
When an item does have a date value, it is returned from the API like this Aug 12 2020 12:00AM
I want to disregard the time and append the date to the date picker when the there is a date present. If there is no date, I want to append todays date.
The following code will display a TextField or a DatePicker based on the value of FieldType.
Here is my API request:
const [details, setDetails] = useState("");
const fetchDetails = async () => {
setBusy(true);
setDetails(await fetch(`/fiscalyears/FY2023/intakes/${params.id}/details`).then((response) => response.json()));
setBusy(false);
};
This is how I switch between TextFields, Selects and the DatePicker:
return (
<Box>
{details["fields"]?.map((row, index) => {
if (row?.FieldType === "Text" || row?.FieldType === "Decimal" || row?.FieldType === "Number") {
return (
<TextField
value={row?.Value || ""}
onChange={(e) => {
setDetails((prev) => {
const update = [...prev.fields];
update[index] = {
...update[index],
Value: e.target.value,
};
return { ...prev, fields: update };
});
}}
/>
);
}
if (row?.FieldType === "Date") {
return (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DatePicker
label={row?.FieldName || ""}
renderInput={(params) => <TextField {...params} />}
value={row?.Value || ""}
onChange={(e) => {
setDetails((prev) => {
const update = [...prev.fields];
update[index] = {
...update[index],
Value: e.target.value,
};
return { ...prev, fields: update };
});
}}
/>
</LocalizationProvider>
);
}
})}
</Box>
)
In the middle of a slightly long component which is basically a split date input (3 inputs for DD/MM/YYYY) - I think my approach is correct for the most part, but I'm struggling with my getDate function. The idea is that the currentDate is passed into this function (whatever has been entered in the keyboard), like so:
const handleValueChange = (date?: string) => {
onValueChange(getDate(date));
};
In the getDate function I want to take the values from selectedDay, selectedMonth & selectedYear and assign them to variables so that they can then be joined using the .join method and a valid date like "28-08-2022" can be returned.
Can anyone advise on a best possible approach?
const getDate = (date?: string): string => {
if (date) {
/* need to take value for DD/ MM / YYYY and store them in
individual variables that can then be used in an array or
something similar with .join to return a
joined up date like "28-09-2022" */
}
return "";
};
const DateInput = ({
dateInputValue,
dateInputFormat,
className,
inFilterContext,
onChange,
onValueChange,
value = "",
type = "text",
readOnly = false,
disabled = false,
placeholder,
mindate,
maxdate,
...inputProps
}: Props) => {
const [intialDayValue, initialMonthValue, initialYearValue] =
dateInputValue.split("-");
const [selectedDay, setSelectedDay] = useState(intialDayValue || "");
const [selectedMonth, setSelectedMonth] = useState(initialMonthValue || "");
const [selectedYear, setSelectedYear] = useState(initialYearValue || "");
const handleValueChange = (date?: string) => {
onValueChange(getDate(date));
};
// handle date input change event - DAY
const handleDayChangeEvent = (e: SyntheticInputEvent<any>) => {
const currentDate = e.target.value;
setSelectedDay(currentDate);
handleValueChange(currentDate);
};
// handle date input change event - MONTH
const handleMonthChangeEvent = (e: SyntheticInputEvent<any>) => {
const currentDate = e.target.value;
setSelectedMonth(currentDate);
handleValueChange(currentDate);
};
// handle date input change event - YEAR
const handleYearChangeEvent = (e: SyntheticInputEvent<any>) => {
const currentDate = e.target.value;
setSelectedYear(currentDate);
handleValueChange(currentDate);
};
return (
<StyledInputGroup>
<label htmlFor={`${name}_day`}>
<span>Day</span>
<StyledInput
{...inputProps}
type={type}
maxLength="2"
disabled={disabled}
readOnly={readOnly}
value={selectedDay}
onChange={handleDayChangeEvent}
onValueChange={handleDateSelectDay}
/>
</label>
<label htmlFor={`${name}_month`}>
<span>Month</span>
<StyledInput
{...inputProps}
type={type}
maxLength="2"
disabled={disabled}
readOnly={readOnly}
value={selectedMonth}
onChange={handleMonthChangeEvent}
onValueChange={handleDateSelectMonth}
/>
</label>
<label htmlFor={`${name}_year`}>
<span>Year</span>
<StyledInput
{...inputProps}
type={type}
maxLength="4"
disabled={disabled}
readOnly={readOnly}
value={selectedYear}
onChange={handleYearChangeEvent}
/>
</label>
</StyledInputGroup>
);
I am currently building a scheduling app. If a user selects two dates, I am attempting to select all date blocks between the two selected dates in the calendar as well. I am able to achieve this, but it causes my useEffect to fire into an infinite loop because I have state as a dependency in my useEffect where I am setting state. I am unsure of the best method to prevent the infinite loop behavior. The useEffect in question is the bottom one. My code is as follows:
export default function App() {
const [selectedDate, handleDateChange] = useState(
dayjs().format("YYYY-MM-DD")
);
const [events] = useState([
{
id: "5e24d1fa-aa66-4122-b1eb-97792f0893b0",
name: "Rodriquez Family",
selectedDates: ["2021-05-01"],
status: "submitted"
},
{
id: "269a0381-63c7-4ab6-92d8-7f7b836aee6f",
name: "Test Family",
selectedDates: ["2021-05-03"],
status: "submitted"
}
]);
const [data, setData] = useState([]);
const getDaysArray = async (firstDay, lastDay) => {
let dates = [];
var dow = dayjs(firstDay).day();
while (dow > 0) {
dates.push(null);
dow = dow - 1;
}
while (firstDay <= lastDay) {
dates.push(firstDay);
firstDay = dayjs(firstDay).add(1, "days").format("YYYY-MM-DD");
}
return dates;
};
useEffect(() => {
const getDates = async () => {
const firstDay = dayjs(selectedDate)
.startOf("month")
.format("YYYY-MM-DD");
const lastDay = dayjs(firstDay).endOf("month").format("YYYY-MM-DD");
const dates = await getDaysArray(firstDay, lastDay);
const list = dates.map((date) => {
const event = events.find(({ selectedDates = [] }) =>
selectedDates.includes(date)
);
return event ? { date, event } : { date, event: null, checked: false };
});
setData(list);
};
getDates();
}, [events, selectedDate]);
const selectDate = (date) => {
setData(
(a) =>
a &&
a.map((item) =>
item.date === date ? { ...item, checked: !item.checked } : item
)
);
};
useEffect(() => {
if (data && data.filter((res) => res.checked).length > 1) {
const filterDates = data.filter((r) => r.checked);
const startDate = filterDates[0].date;
const endDate = filterDates[filterDates.length - 1].date;
const datesToUpdate = data.filter(
(res) => res.date > startDate && res.date < endDate
);
const newArr = data.map((date) => {
const updateCheck = datesToUpdate.find((r) => r.date === date.date);
return updateCheck ? { ...updateCheck, checked: true } : date;
});
setData(newArr);
}
}, [data]);
return (
<MuiPickersUtilsProvider utils={DayJsUtils}>
<div className="App">
<DatePicker
minDate={dayjs()}
variant="inline"
openTo="year"
views={["year", "month"]}
label="Year and Month"
helperText="Start from year selection"
value={selectedDate}
onChange={handleDateChange}
/>
</div>
<div className="cal">
<div className="cal-div1"></div>
<div className="cal-div2 "></div>
<div className="cal-div3 cal-cir-hov"></div>
<div className="cal-div4"> SUN </div>
<div className="cal-div5"> MON </div>
<div className="cal-div6"> TUE </div>
<div className="cal-div7"> WED </div>
<div className="cal-div8"> THU </div>
<div className="cal-div9"> FRI </div>
<div className="cal-div10"> SAT </div>
{data &&
data.map((r, i) => {
return (
<>
<div
onClick={() =>
!r.checked &&
r.date >= dayjs().format("YYYY-MM-DD") &&
!r.event &&
selectDate(r.date)
}
style={
r.checked
? { backgroundColor: "green" }
: { color: "#565254" }
}
key={i}
className="cal-cir-hov"
>
<div>{r.date} </div>
<div
style={
r.event?.status === "submitted"
? { color: "orange" }
: { color: "green" }
}
>
{r.event?.name}
</div>
</div>
</>
);
})}
</div>
</MuiPickersUtilsProvider>
);
}
attached is a code sandbox for debugging and to show the behavior I am currently talking about. Select two separate dates that are greater than today and you will see all the dates in between are selected, but the app goes into a loop https://codesandbox.io/s/dawn-snow-03r59?file=/src/App.js:301-4499
If your useEffect depends on a variable that you're updating on the same useEffect there will always be the re-render and cause a loop.
If you want it to execute only once, you should remove the data variable from the useEffect dependency array.
But if you really wanna mutate the state every time that the data variable changes, my recommendation is to create another state for the mutated data.
For example setFormattedData would not change the data itself, but you would still have a state for this data in the format that you want.
I can't get my component to show my autosuggestions.
It is observed in the console that my data is available and I sent it to this component using the suggestions prop, using Material UI AutoComplete component feature here I am trying to set my options, and these are changing as I type as it's handled in a parent component, but setting the values does not seem to reflect nor bring up my suggestions. I am very confused. my code is below.
import React, { FunctionComponent, FormEvent, ChangeEvent } from "react";
import { Grid, TextField, Typography } from "#material-ui/core";
import { CreateProjectModel, JobModel } from "~/Models/Projects";
import ErrorModel from "~/Models/ErrorModel";
import Autocomplete from "#material-ui/lab/Autocomplete";
type CreateProjectFormProps = {
model: CreateProjectModel;
errors: ErrorModel<CreateProjectModel>;
onChange: (changes: Partial<CreateProjectModel>) => void;
onSubmit?: () => Promise<void>;
suggestions: JobModel[];
};
const CreateProjectForm: FunctionComponent<CreateProjectFormProps> = ({
model,
errors,
onChange,
onSubmit,
suggestions,
}) => {
const [open, setOpen] = React.useState(false);
const [options, setOptions] = React.useState<JobModel[]>([]);
const loading = open && options.length === 0;
const [inputValue, setInputValue] = React.useState('');
React.useEffect(() => {
let active = true;
if (!loading) {
return undefined;
}
(async () => {
if (active) {
setOptions(suggestions);
}
})();
return () => {
active = false;
};
}, [loading]);
React.useEffect(() => {
if (!open) {
setOptions([]);
}
}, [open]);
const submit = async (event: FormEvent) => {
event.preventDefault();
event.stopPropagation();
await onSubmit();
};
const change = (name: string) => (event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
onChange({
[name]: event.target.value,
});
};
const getFieldProps = (id: string, label: string) => {
return {
id,
label,
helperText: errors[id],
error: Boolean(errors[id]),
value: model[id],
onChange: change(id),
};
};
return (
<Autocomplete
{...getFieldProps}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionSelected={(option, value) => option.id === value.id}
getOptionLabel={(option) => option.id}
options={options}
loading={loading}
autoComplete
includeInputInList
renderInput={(params) => (
<TextField
{...getFieldProps("jobNumber", "Job number")}
required
fullWidth
autoFocus
margin="normal"
/>
)}
renderOption={(option) => {
return (
<Grid container alignItems="center">
<Grid item xs>
{options.map((part, index) => (
<span key={index}>
{part.id}
</span>
))}
<Typography variant="body2" color="textSecondary">
{option.name}
</Typography>
</Grid>
</Grid>
);
}}
/>
);
};
export default CreateProjectForm;
Example of my data in suggestions look like this:
[{"id":"BR00001","name":"Aircrew - Standby at home base"},{"id":"BR00695","name":"National Waste"},{"id":"BR00777B","name":"Airly Monitor Site 2018"},{"id":"BR00852A","name":"Cracow Mine"},{"id":"BR00972","name":"Toowoomba Updated"},{"id":"BR01023A","name":"TMRGT Mackay Bee Creek"},{"id":"BR01081","name":"Newman Pilot Job (WA)"},{"id":"BR01147","name":"Lake Vermont Monthly 2019"},{"id":"BR01158","name":"Callide Mine Monthly Survey 2019"},{"id":"BR01182","name":"Lake Vermont Quarterly 2019 April"}]
The problem in your code are the useEffects that you use.
In the below useEffect, you are actually setting the options to an empty array initially. That is because you autocomplete is not open and the effect runs on initial mount too. Also since you are setting options in another useEffect the only time your code is supposed to work is when loading state updates and you haven't opened the autocomplete dropdown.
The moment you close it even once, the state is updated back to empty and you won't see suggestions any longer.
React.useEffect(() => {
if (!open) {
setOptions([]);
}
}, [open]);
The solution is simple. You don't need to keep a local state for options but use the values coming in from props which is suggestions
You only need to keep a state for open
const CreateProjectForm: FunctionComponent<CreateProjectFormProps> = ({
model,
errors,
onChange,
onSubmit,
suggestions,
}) => {
const [open, setOpen] = React.useState(false);
const loading = open && suggestions.length === 0;
const [inputValue, setInputValue] = React.useState('');
const submit = async (event: FormEvent) => {
event.preventDefault();
event.stopPropagation();
await onSubmit();
};
const change = (name: string) => (event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
onChange({
[name]: event.target.value,
});
};
const getFieldProps = (id: string, label: string) => {
return {
id,
label,
helperText: errors[id],
error: Boolean(errors[id]),
value: model[id],
onChange: change(id),
};
};
return (
<Autocomplete
{...getFieldProps}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionSelected={(option, value) => option.id === value.id}
getOptionLabel={(option) => option.id}
options={suggestions}
loading={loading}
autoComplete
includeInputInList
renderInput={(params) => (
<TextField
{...getFieldProps("jobNumber", "Job number")}
required
fullWidth
autoFocus
margin="normal"
/>
)}
renderOption={(option) => {
return (
<Grid container alignItems="center">
<Grid item xs>
{options.map((part, index) => (
<span key={index}>
{part.id}
</span>
))}
<Typography variant="body2" color="textSecondary">
{option.name}
</Typography>
</Grid>
</Grid>
);
}}
/>
);
};
export default CreateProjectForm;
i noticed a few issues with your code, getFieldProps is being called without the id or name params which cause the page to not load. More importantly, you should consider following the autocomplete docs when passing and using props to it. for example:
renderInput={(params) => <TextField {...params} label="Controllable" variant="outlined" />}
i asked a few questions, pls let me know when you can get those answers so i may address all the issues that may come up.
Q1. should the user input provide relevant matches from the name property in your suggestions or just the id? for ex. if i type "lake", do you want to show BRO1182, Lake Vermont Quarterly 2019 April as a match?
Q2. how did you want to address the error case? i see you have a error model, but unsure how you wish to use it to style the autocomplete when an error occurs
Q3. are we missing a submit button? i see the onSubmit function but it's not used in our code.
Q4. is there a particular reason why you need the open and loading states?
below is what i attempted so far to show related matches from user input
import React, { FunctionComponent, FormEvent, ChangeEvent } from "react";
import { Grid, TextField, Typography } from "#material-ui/core";
import { CreateProjectModel, JobModel } from "~/Models/Projects";
import ErrorModel from "~/Models/ErrorModel";
import Autocomplete from "#material-ui/lab/Autocomplete";
type CreateProjectFormProps = {
model: CreateProjectModel;
errors: ErrorModel<CreateProjectModel>;
onChange: (changes: Partial<CreateProjectModel>) => void;
onSubmit?: () => Promise<void>;
suggestions: JobModel[];
};
const CreateProjectForm: FunctionComponent<CreateProjectFormProps> = ({
model,
errors,
// mock function for testing
// consider a better name like selectChangeHandler
onChange = val => console.log(val),
// consider a better name like submitJobFormHandler
onSubmit,
suggestions: options = [
{ id: "BR00001", name: "Aircrew - Standby at home base" },
{ id: "BR00695", name: "National Waste" },
{ id: "BR00777B", name: "Airly Monitor Site 2018" },
{ id: "BR00852A", name: "Cracow Mine" },
{ id: "BR00972", name: "Toowoomba Updated" },
{ id: "BR01023A", name: "TMRGT Mackay Bee Creek" },
{ id: "BR01081", name: "Newman Pilot Job (WA)" },
{ id: "BR01147", name: "Lake Vermont Monthly 2019" },
{ id: "BR01158", name: "Callide Mine Monthly Survey 2019" },
{ id: "BR01182", name: "Lake Vermont Quarterly 2019 April" }
]
}) => {
const [value, setValue] = React.useState<JobModel>({});
const loading = open && options.length === 0;
// this pc of code is not used, why?
const submit = async (event: FormEvent) => {
event.preventDefault();
event.stopPropagation();
await onSubmit();
};
const handleChange = (_: any, value: JobModel | null) => {
setValue(value);
onChange({
[value.name]: value.id
});
};
// consider passing in props instead
const getFieldProps = (id: string, label: string) => {
return {
id,
label,
// not sure what this is
helperText: errors[id],
// not sure what this is
error: Boolean(errors[id]),
value: model[id],
onChange: change(id)
};
};
return (
<Autocomplete
id="placeholder-autocomplete-input-id"
// for selection, use value see docs for more detail
value={value}
onChange={handleChange}
getOptionSelected={(option, value) => option.id === value.id}
getOptionLabel={option => option.id}
options={options}
loading={loading}
autoComplete
includeInputInList
renderInput={params => (
// spreading the params here will transfer native input attributes from autocomplete
<TextField
{...params}
label="placeholder"
required
fullWidth
autoFocus
margin="normal"
/>
)}
renderOption={option => (
<Grid container alignItems="center">
<Grid item xs>
<span key={option}>{option.id}</span>
<Typography variant="body2" color="textSecondary">
{option.name}
</Typography>
</Grid>
</Grid>
)}
/>
);
};
export default CreateProjectForm;
and you can see the code running in my codesandbox by clicking the button below
If I understand your code and issue right, you want -
React.useEffect(() => {
let active = true;
if (!loading) {
return undefined;
}
(async () => {
if (active) {
setOptions(suggestions);
}
})();
return () => {
active = false;
};
}, [loading]);
to run each time and update options, but the thing is, [loading] dependency setted like
const loading = open && suggestions.length === 0;
and not gonna trigger changes.
Consider doing it like so -
const loading = useLoading({open, suggestions})
const useLoading = ({open, suggestions}) => open && suggestions.length === 0;
please find below code which contains name id and am rendering initially using map
am replacing id value to input type in UI
with the updated input type am trying to update the value onchange
update is not capturing and unable to update the input field
any suggestion?
please refer below snippet
import React, { useState } from "react";
const CstmInput = (props) => {
return (
<input
name={props.name}
type="text"
value={props.value}
onChange={(event) => props.onInputChange(event)}
/>
);
};
export default CstmInput;
import React, { useState } from "react";
import CstmInput from "./CstmInput";
const HierarcyTest = () => {
let rowData = [
{ name: "first", id: 10 },
{ name: "second", id: 20 },
];
const [data, setData] = useState(rowData);
const [name, setName] = useState({ fn: "test" });
const onInputChange = (e) => {
console.log("---event---", e.target.value);
setName({ ...name, fn: e.target.value });
};
let updateValue = () => {
let newData = data.map(
(item, index) =>
(item.id = (
<CstmInput name={item.name} value={item.id} onInputChange={(e) => onInputChange(e)} />
))
);
setData([...data, newData]);
};
return (
<div>
<div>Testing</div>
{data.map((val) => (
<h6>
{" "}
{val.name} {val.id}
</h6>
))}
<button onClick={updateValue}> Click </button>
</div>
);
};
export default HierarcyTest;
A few things why your code isn't working as intended:
1.
let updateValue = () => {
let newData = data.map((item, index) => {
if (item.id === 10) {
return [
(item.id = (
<CstmInput
value={item.id}
onInputChange={(e) => onInputChange(e)}
/>
)),
];
}
});
setData([...data, newData]);
};
In the above function inside the callback of map, you're only returning when a condition satisfies. Are you trying to filter the array instead? If not then return something when the if condition fails.
And why are you returning an array?
return [
(item.id = (
<CstmInput
value={item.id}
onInputChange={(e) => onInputChange(e)}
/>
)),
];
the above code seems logically wrong.
2.
const onInputChange = (e) => {
console.log("---event---", e.target.value);
setName({ ...name, fn: e.target.value });
};
If you want to update state which depends on the previous state then this is how you do it:
setName((prevState) => ({ ...prevState, fn: e.target.value }));
but since you're not actually relying on the properties of the previous state you can just use:
setName({fn: e.target.value });
Note that since your state only has one property and you want to update that single property you can completely overwrite the state, you don't need to spread the previous state.
update
change the updateValue function as the following:
let updateValue = () => {
setData(prevData => {
return prevData.map(el => {
return { ...el, id: <CstmInput value={el.id} onInputChange={(e) => onInputChange(e)} /> };
})
});
};
A stackblitz example I've created that implements what you're trying to do.