How do I edit multiple text fields with a unified state/handler? - javascript

Background:
I am successfully able to use separate states/handlers to populate and edit my text fields, but I would like to simplify/condense my code by creating a unified state and a unified handler for many of my text fields.
Problem:
I am successfully able to obtain the correct values with useEffect, but when I try to edit one of the text fields I get an error message saying: TypeError: Cannot read property 'value' of undefined
My Code:
useEffect(() => {
getDataFromApi(props.id).then((rsp) => {
setData(rsp);
textFields.firstItem = rsp.content?.firstItem;
textFields.secondItem = rsp.content?.secondItem;
});
}, []);
const [textFields, setTextFields] = useState({
firstItem: "",
secondItem: "",
});
const handleChangeTextField = (type) => (e) => {
setTextFields((prevState) => ({
...prevState,
[type]: e.target.value,
}));
};
<TextField
id="firstItem"
value={textFields.firstItem}
onChange={handleChangeTextField("firstItem")}
/>
<TextField
id="secondItem"
value={textFields.secondItem}
onChange={handleChangeTextField("secondItem")}
/>
Any suggestions on what I am doing wrong would be helpful!
Thanks.

If you use that try like this.
const [textFields, setTextFields] = useState({
firstItem: "",
secondItem: ""
});
const handleChangeTextField = (type) => (value) => {
setTextFields((prevState) => ({
...prevState,
[type]: value
}));
};
<TextField
id="firstItem"
variant="filled"
value={textFields.firstItem}
onChange={handleChangeTextField("firstItem")}
/>
<TextField
id="secondItem"
value={textFields.secondItem}
variant="filled"
onChange={handleChangeTextField("secondItem")}
/>
You can confirm here. https://codesandbox.io/s/wizardly-davinci-3x0ui?file=/App.js:0-865

Related

useState variables not retaining values assigned to them after api call - React Js

Edited
I am trying to make a form that will enable the user to edit an existing citation. The citation is pulled from the backend with an api call. I then assign the values grabbed from the api to the state variables. These state variables is passed as props to the form jsx where they are used in the value prop of the input field. But for some reason the values assigned to state is not retained by the state and it goes back to its initial state values. I am adding the code as to how the component looks like
const EditEnterCitation = () => {
const [searchParams, setSearchParams] = useSearchParams();
// getting params from the url to pass to the functions
const cat = searchParams.get("cat");
const { id } = useParams();
// state to pass to the value prop in the form input
const [formData, setFormData] = useState({
institution_name: "0",
judge_name: "",
case_no: "",
apelLate_type: "0",
law: "",
point_of_law: "0",
party_name_appelant: "",
party_name_respondent: "",
advocate_petitioner: "",
advocate_respondent: "",
judgements: "",
date_of_order: "",
headnote: "",
references: "",
equivalent_citations: "",
title: "",
});
// state to for select buttonn in the form
const [instName, setInstName] = useState([]);
const [lawChoiceOptions, setlawChoiceOptions] = useState([]);
const [pointOfLawOptions, setPointOfLawOptions] = useState([]);
const [appealateType, setAppealateType] = useState([]);
// getting the choices for select from backend
const allChoices = useQuery(
["allChoices", cat],
() => getAllChoices(cat),
{
enabled: cat !== null,
refetchOnWindowFocus: false,
onSuccess: (response) => {
// setting law choices
if (response.data.law) {
let arr = response.data.law.map((element, index) => {
return { value: element.law_name, label: element.law_name };
});
setlawChoiceOptions(arr);
}
// setting point of law options
setPointOfLawOptions(response.data.pol);
// setting appealate type
setAppealateType(response.data.appeal);
// setting ins choices
if (response.data.ins.length) {
setInstName(response.data.ins);
} else {
let court_names = [];
let tribunal_name = [];
if (response.data.ins.ins_court.length > 0) {
court_names = response.data.ins.ins_court;
}
if (response.data.ins.ins_tribunal.length > 0) {
tribunal_name = response.data.ins.ins_tribunal;
}
let ins_names = court_names.concat(tribunal_name);
setInstName(ins_names);
}
},
}
);
// function to get default values of the citation
const getDefaultValues = useQuery(
["detailCitation", id],
() => detailCitation(cat, id),
{
enabled: allChoices.isFetched === true,
refetchOnWindowFocus: false,
onSuccess: (response) => {
setFormData({
institution_name: response.data.institution_name,
judge_name: response.data.judge_name,
case_no: response.data.case_no,
apelLate_type: response.data.apelLate_type,
law: response.data.law,
point_of_law: response.data.point_of_law,
party_name_appelant: response.data.party_name_appelant,
party_name_respondent: response.data.party_name_respondent,
advocate_petitioner: response.data.advocate_petitioner,
advocate_respondent: response.data.advocate_respondent,
judgements: response.data.judgements,
date_of_order: response.data.date_of_order,
headnote: response.data.headnote,
references: response.data.references,
equivalent_citations: response.data.equivalent_citations,
title: response.data.title,
});
},
}
);
// handling form data change for some fields
const handleFormDataChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
// handling form data change for the judgement
const handleJudgementData = (data) => {
setFormData({ ...formData, judgements: data });
};
const handlePartyAppealData = (data) => {
setFormData({ ...formData, party_name_appelant: data });
};
const handlePartyRespondData = (data) => {
setFormData({ ...formData, party_name_respondent: data });
};
// handling data change of the law choices
const handleOnChangeLaw = (selectedOption) => {
let newArray = [];
selectedOption.map((element) => {
newArray.push(element.value);
});
setFormData({ ...formData, law: newArray.toString() });
};
// hadling sumbimission of citation
const handleFormDataSubmit = async (e) => {
e.preventDefault();
try {
setProgress(80);
let response = await addCitation(cat, formData);
if (response.status === 201) {
toastNotification(
`Citation Uploaded to ${cat.toUpperCase()}`,
`success`
);
setProgress(100);
goToTop();
} else {
toastNotification(`Server Error. Could not upload citation`, `error`);
setProgress(100);
}
} catch (error) {}
};
useEffect(() => {
console.count("formData Appearing");
console.log(formData);
});
return (
<>
<LoadingBar
color="red"
progress={progress}
onLoaderFinished={() => setProgress(0)}
height={4}
/>
<Helmet>
<title>Enter Citation</title>
</Helmet>
<Wrapper>
<FormContainer onSubmit={handleFormDataSubmit}>
<EditInsFormElements
cat={cat}
handleFormDataChange={handleFormDataChange}
formData={formData}
handleJudgementData={handleJudgementData}
handleOnChangeLaw={handleOnChangeLaw}
instName={instName}
/>
<EditOtherFormElements
cat={cat}
handleFormDataChange={handleFormDataChange}
formData={formData}
handleJudgementData={handleJudgementData}
handleOnChangeLaw={handleOnChangeLaw}
lawChoiceOptions={lawChoiceOptions}
pointOfLawOptions={pointOfLawOptions}
appealateType={appealateType}
handlePartyAppealData={handlePartyAppealData}
handlePartyRespondData={handlePartyRespondData}
/>
<FormFooter>
<UploadBtn disabled={disableSubmit()} type="submit">
Upload Citation
</UploadBtn>
</FormFooter>
</FormContainer>
</Wrapper>
</>
);
};
the jsx inside the EditInsFormElements and EditOtherFormElements looks like this
<label className="required-field" htmlFor="apelLate_type">
Apellate Type*
</label>
<select
name="apelLate_type"
id="apelLate_type"
value={formData.apelLate_type}
onChange={handleFormDataChange}
required
>
<option value="0" disabled hidden>
Select
</option>
{appealateType &&
appealateType.length &&
appealateType.map((element, index) => {
return (
<option key={index} value={element.appealate_type}>
{element.appealate_type}
</option>
);
})}
</select>
<label className="required-field" htmlFor="case_no">
Case No*
</label>
<TextArea
name="case_no"
id="case_no"
value={formData.case_no}
onChange={handleFormDataChange}
required
/>
<label className="required-field" htmlFor="title">
Title*
</label>
<input
type="text"
name="title"
value={formData.title}
onChange={handleFormDataChange}
required
/> ....
And the state behaviour in the console that I have produced using useEffect is attached below
Question
Why is this alternating behaviour seen in the console is caused and how to rectify it so that the state retains the values grabbed and assigned from the backend.
So after debugging I founf out that the problem was with the CkEditor component I was using, whose onChange function was creating this behaviour.
the way I was using the onChange function on one of the ckeditor component was like below
<CKEditor
editor={Editor}
data={formData.judgements}
name="judgements"
id="judgements"
onChange={(event, editor, e) => {
const data = editor.getData()
handleJudgementsData(data)
} }
/>
and the onChange function looked like this
const handleJudgementData = (data) => {
setFormData({ ...formData, judgements: data });
};
This was causing the problem as the state was not updating synchronously what I could understand from Phil's answer in the question he linked
so I changed the code in the below way and now it is working
jsx
<CKEditor
editor={Editor}
data={formData.judgements}
onChange={(event, editor, e) => handleJudgementData(event, editor, e)}
/>
onChnage function
const handleJudgementData = (event, editor) => {
let data = editor.getData();
setFormData((prev) => ({ ...prev, judgements: data }));
};
After doing it the above way now the state retains the data fetched from api

React Material UI Textfield onChange

const [formData, setFormData] = useState({});
useEffect(() => {
fetch("/formdata")
.then((res) => res.json())
.then((data) => setFormData(data));
}, []);
console.log("Form Data", formData);
//Sorting by order
let attr;
form.forms.map((y) => {
return (attr = y.formAttributes.sort((a, b) => {
return a.order < b.order ? -1 : 1;
}));
});
return (
{attr.map((attri, index) => {
return (
<TextField
key={index}
label={attri.label}
value={formData[attri.datakey] || ""}
onChange={event => {const {value} = event.target; setFormData({formData: value})}}
/>
);
})}
)
I would like to ask help on how to manage multiple fields in Textfield onChange area? Currently, if I am going to input a value there are no changes that is happening.
Here's my code.
Tried the approach of using e.target.value however it would still stay the same.
You should add a name attribute, where the 'name' is the key of the key-value-pair in your object.
e.g.
<TextField
key={index}
name="somename"
label={attri.label}
value={formData[attri.datakey] || ""}
onChange={handleChange}
/>
Then you can update the field like that.
setFormData({
...formData,
[e.target.name]: e.target.value // becomes "somename: some value"
})

React js Validation textfield

I am trying to make a validation in reactjs.
For example, I have a field called "name" and every time you input a name, this has to be bigger or equal to 2. It works when the name is smaller than 2, but if it's bigger than 2 the message still appears.
Here is my code:
const [data, setData] = React.useState({
nameValid: true,
prenumeValid: true,
check_textInputChange: false,
name: "",
});
const handleValidUser = (val) => {
if (val.length >= 2) {
setData({
...data,
nameValid: true,
});
} else {
setData({
...data,
nameValid: false,
});
}
};
const textInputChange = (val) => {
if (val.length >= 2) {
setData({
...data,
name: val,
nameValid: true,
check_textInputChange: true,
});
} else {
setData({
...data,
name: val,
nameValid: false,
check_textInputChange: false,
});
}
};
<TextField
type="text"
variant="outlined"
label="Nume"
required
fullWidth
autofocus
validate
style={{ marginBottom: "1em" }}
onChange={(val) => textInputChange(val)}
onBlur={(e) => handleValidUser(e)}
/>
{data.nameValid ? null : (
<text>The name has to be at least 2 letters long</text>
)}
onChange has an event parameter, if you want to get the value of the TextField, you should write it as below.
const textInputChange = e => {
const val = e.target.value;
if (val.length > 2) {
setData(...)
} else {
setData(...)
}
}
return (
<TextField
...
onChange={textInputChange}
onBlur={handleValidUser} // as well as onBlur
/>
)
I've created a codesandbox to demonstrate here.
Call the function as below will create an extra render every time the function is called. You should avoid it.
onChange={(val) => textInputChange(val)}
Can you try changing
onChange={(val) => textInputChange(val)}
to this:
onChange={(e) => textInputChange(e.target.value)}
in function textInputChange you are passing event object instead of input value
you have to change onchange handler from:
onChange={(val) => textInputChange(val)}
to
onChange={(val) => textInputChange(val.target.value)}

MaterialUI Compoent does not AutoComplete when data is available

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;

How to properly update an array inside a react hook state

i've been trying to update the object inside an array that respresents a react state, the object should be updated when the value of an input is changed, I could find a way myself to update it, but i'm not sure enough that it is the proper way to do it because when i open the react dev tools and go to the components tab and click the component that i'm working on, the state doesn't update immidiatly when typing in the input, and in order to see the change i have to click on another component in the dev tool and then go back to the first component and the change is done.
So I'm basically asking if the way i used to update the state is correct and to get some suggestions about better ways to do it so it updates instantly. Thanks
here is the code
the state:
const [items, setItems] = useState([{ name: "", quantity: "", unit: "" }]);
the change handling function (the function that updates the state):
const nameChange = (e, i) => {
const newItems = items;
newItems[i].name = e.target.value;
setItems(newItems);
console.log(items);
};
the inputs:
{
items.map((item, i) => {
return (
<div key={i} className={`mt3 ${classes.root}`}>
<TextField
onChange={e => nameChange(e, i)}
style={{ width: "30%" }}
id="standard-basic"
label="Item name"
/>
<TextField
style={{ width: "25%" }}
id="standard-basic"
label="quantity"
/>
<TextField
style={{ width: "10%" }}
id="standard-basic"
label="Unit"
/>
</div>
);
});
}
const [items, setItems] = React.useState({
name: '',
quantity: '',
unit: ''
});
const handleChange = prop => event => {
setItems({ ...items, [prop]: event.target.value });
};
on your TextFields:
<TextField onChange={handleChange('name')}/>
<TextField onChange={handleChange('quantity')}/>
Update: if items is an array:
const updateItem = (prop, event, index) => {
const old = items[index];
const updated = { ...old, [prop]: event.target.value }
const clone = [...items];
clone[index] = updated;
setItems(clone);
}
on your TextFields:
{items.map((item, i) => (
<div key={i}>
<TextField onChange={e => updateItem('name', e, i)}/>
<TextField onChange={e => updateItem('quantity', e, i)}/>
<TextField onChange={e => updateItem('unit', e, i)}/>
</div>
))}
I got the issue when state is object. I used redux and mapStateToProps and I can't listen the property in object change cause react can not check deeper.
Solution: follow code:
// in reducer file
case 'SAVE_SERVICE_STATE': {
return {
...state,
service: {...payload},
loading: false,
};
}
//listen service state change
useEffect(() => {
if (props.serviceState) {
// do something
}
}, [props.serviceState]);
hope can help someone.

Categories

Resources