How to debounce multiple fields to avoid rerender in React - javascript

I have multiple text fields to save values automatically when users start typing in React. I want to use setTimeOut inside the onChange function to avoid re-render/api call every time a user types a character. The issue is that the clearTimeOut will clear everything afterward even if I'm typing in different fields so I can only catch the latest value of the latest field that the mouse focus is in instead of all values from each text field. Please let me know is this the right way to achieve it. Thanks!
const [values, setValues] = useState([]);
useEffect(() => {
//call API
}, [values]);
let timer: any;
const onChange =
(index) =>
(event) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
let temp = [...values];
temp[index] = event.target.value;
setValues(temp);
}, 2000);
};
return <div>
{data?.map((field, index) => (
<TextField
key={index}
variant="outlined"
onChange={onChange(field.index)}
/>
))}
</div>

Related

Stop checkbox values being reset after page load

Hello I am working on a pop up window where the user can filter a table of data.
The filter is selected using checkboxes.
My issue:
On page load there is a useEffect that changes every checkbox to false. This is based on the props coming in from the API.
I'd like on page load (and when the filter opens) that the checkbox state is stored based on what the user has selected previously in their session
code:
Filter component*
[...]
import FilterSection from "../FilterSection";
const Filter = ({
open,
handleClose,
setFilterOptions,
[..]
roomNumbers,
}) => {
const [roomValue, setRoomValue] = React.useState();
const [roomListProp, setRoomListProp] = React.useState(); // e.g. [["roomone", false], ["roomtwo", true]];
const sendRoomFilterData = (checkedRoomsFilterData) => {
setRoomValue(checkedRoomsFilterData);
};
const setCheckboxListPropRoom = (data) => {
setRoomListProp(data);
};
// extract, convert to an object and pass back down? or set local storage and get
// local storage and pass back down so that we can get it later?
const convertToLocalStorageFilterObject = (roomData) => { // []
if (roomData !== undefined) {
const checkedRooms = roomData.reduce((a, curval) => ({ ...a, [curval[0]]: curval[1] }), {});
localStorage.setItem("preserved", JSON.stringify(checkedRooms)); // sets in local storage but values get wiped on page load.
}
};
React.useEffect(() => {
const preservedFilterState = convertToLocalStorageFilterObject(roomListProp);
}, [roomListProp]);
const applyFilters = () => {
setFilterOptions([roomValue]);
handleClose();
};
const classes = CurrentBookingStyle();
return (
<Dialog
fullWidth
maxWidth="sm"
open={open}
onClose={() => handleClose(false)}
>
<DialogTitle>Filter By:</DialogTitle>
<DialogContent className={classes.margin}>
<FilterSection
filterName="Room number:"
filterData={roomNumbers}
setFilterOptions={sendRoomFilterData}
setCheckboxListProp={setCheckboxListPropRoom}
/>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={applyFilters}>
Apply Filters
</Button>
</DialogActions>
</Dialog>
);
};
Filter Section used in Filter
import {
TableCell,
Typography,
FormControlLabel,
Checkbox,
FormGroup,
} from "#material-ui/core";
const FilterSection = ({
filterData, filterName, setFilterOptions, setCheckboxListProp
}) => {
const [checkboxValue, setCheckboxValue] = React.useState({});
const [checkboxFilterList, setCheckboxFilterList] = React.useState([]);
const handleCheckboxChange = (event) => {
setCheckboxValue({
...checkboxValue,
[event.target.name]: event.target.checked, // room1: true
});
};
const = () => filterData // ["room1" "room2"]; comes from API
.filter((val) => !Object.keys(checkboxValue).includes(val))
.reduce((acc, currval) => ({
...acc, [currval]: false, // converts array to object and sets values to false
}), checkboxValue);
React.useEffect(() => {
const transformedCheckboxListItems = Object.entries(convertToObject());
setCheckboxFilterList(transformedCheckboxListItems);
setFilterOptions(transformedCheckboxListItems.filter(([, val]) => val).map(([key]) => key));
setCheckboxListProp(transformedCheckboxListItems);
}, [checkboxValue]);
return (
<>
<Typography style={{ fontWeight: "bold" }}>{filterName}</Typography>
<FormGroup row>
{checkboxFilterList.map(([key, val]) => (
<TableCell style={{ border: 0 }}>
<FormControlLabel
control={(
<Checkbox
checked={val}
onChange={handleCheckboxChange}
name={key}
color="primary"
/>
)}
label={key}
/>
</TableCell>
))}
</FormGroup>
</>
);
};
What i have tried:
I have created a reusable component called "FilterSection" which takes takes data from the API "filterData" and transforms it from an array to an object to set the initial state for the filter checkboxes.
On page load of the filter I would like the checkboxes to be true or false depending on what the user has selected, however this does not work as the convertToObject function in my FilterSection component converts everything to false again on page load. I want to be able to change this but not sure how? - with a conditional?
I have tried to do this by sending up the state for the selected checkboxes to the Filter component then setting the local storage, then the next step would be to get the local storage data and somehow use this to set the state before / after page load. Unsure how to go about this.
Thanks in advance
I am not sure if I understand it correctly, but let me have a go:
I have no idea what convertToObject does, but I assume it extracts the saved filters from localStorage and ... updates the filter value that has just been changed?
Each time the FilterSection renders for the first time, checkboxValue state is being initialised and an useEffect runs setCheckboxListProp, which clears the options, right?
If this is your problem, try running setCheckboxListProp directly in the handleCheckboxChange callback rather than in an useEffect. This will ensure it runs ONLY after the value is changed by manual action and not when the checkboxValue state is initialised.
I solved my problem by moving this line:
const [checkboxValue, setCheckboxValue] = React.useState({});
outside of the component it was in because every time the component re-rendered it ran the function (convertToObject() which reset each checkbox to false
by moving the state for the checkboxes up three layers to the parent component, the state never got refreshed when the or component pop up closed. Now the checkbox data persists which is the result I wanted.
:D

How to stop cursor jumps to the end?

I'm using Antd Input library, whenever I type in the start or in the middle of the word my cursor jumps to the end.
const handleOpenAnswer =( key, value )=>{
handleFieldChange({
settings: {
...settings,
[key]: value
}
})
}
return (
<Input
required
size='default'
placeholder='Label for Diference Open Answer Question'
value='value'
onChange={({ target: { value } }) => {
handleOpenAnswer('differenceOpenAnswerLabel', value)
}}
/>
The reason why your cursor always jumps to the end is because your parent component gets a new state and therefore re-renders its child components. So after every change you get a very new Input component. So you could either handle the value change within the component itself and then try to pass the changed value up to the parent component after the change OR (and I would really recommend that) you use something like React Hook Form or Formik to handle your forms. Dealing with forms on your own can be (especially for complex and nested forms) very hard and ends in render issues like you face now.
Example in React-Hook-Form:
import { FormProvider, useFormContext } = 'react-hook-form';
const Form = () => {
const methods = useForm();
const { getValues } = methods;
const onSubmit = async () => {
// whatever happens on submit
console.log(getValues()); // will print your collected values without the pain
}
return (
<FormProvider {...methods}>
<form onSubmit={(e) => handleSubmit(onSubmit)(e)>
{/* any components that you want */}
</form>
</FormProvider>
);
}
const YourChildComponent = () => {
const { register } = useFormContext();
return (
<Input
{...register(`settings[${yourSettingsField}]`)}
size='default'
placeholder='Label for Diference Open Answer Question'
/>
)
}

state hooks is not updating state on change

I am building a simple blog app and I am trying to update title of the blog But it is not updating, it is just showing the current state.
I have tried many times by changing the method of setting state but it is still showing that error.
App.js
function BlogDetail() {
const [blogName, setBlogName] = useState("");
axios.get("/api/blog_detail/70/").then(res => {
setBlogName(res.data[0].blog_name)
})
const handleChange = (e) => {
console.log(e.target.value)
setBlogName({
...blogName,
[e.target.name]: e.target.value
})
}
const saveBlog = (e) => {
// sending to API
console.log(blogName)
}
return (
<div>
<form>
{blogName}
<input type="text" name="blogName" value={blogName} onChange={e => handleChange} />
<button type="submit" onClick={e => saveBlog(e)}>Save</button>
<form>
</div>
)
}
And When I update on change instead of updating on submit
onChange=(e => setBlogName(e.target.value))
Then it is showing
A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined
I have tried many times but it is still not working.
input requires a string as a value, but you are trying to pass an object:
setBlogName({
...blogName,
[e.target.name]: e.target.value
})
instead pass a string:
setBlogName(e.target.value)
Also, you need to execute handleChange function and pass the event param.
onChange={e => handleChange(e)}
Edit:
Looked at it second time and it should be like this:
function BlogDetail() {
const [blogName, setBlogName] = useState("");
// without this you override state every time you press a key
useEffect(() => {
axios.get("/api/blog_detail/70/").then(res => {
setBlogName(res.data[0].blog_name)
})
}, [])
const handleChange = (e) => {
// just use value here
setBlogName(e.target.value)
}
const saveBlog = (e) => {
// sending to API
console.log(blogName)
}
return (
<div>
<form>
{blogName}
{ /* remember to run the function */ }
<input type="text" name="blogName" value={blogName} onChange={e => handleChange()} />
<button type="submit" onClick={e => saveBlog(e)}>Save</button>
<form>
</div>
)
}
Besides the problem that within handleChange you need to pass an an string value to setBlogName you also need to wrap your axios fetch call in a useEffect.
The problem is that everytime you trigger a rerender while calling setBlogName you are calling your API point again and set the value back to the fetched value.
You should prevent that by doing the following ->
useEffect(() => {
axios.get("/api/blog_detail/70/").then(res => {
setBlogName(res.data[0].blog_name)
}), [])
Don't forget to install { useEffect } from 'react'.
And well of course update handleChange ->
const handleChange = (e) => {
const newBlogPostName = e.target.value
console.log(newBlogPostName)
setBlogName(newBlogPostName)
}
you have not any action in this method. where is the update state?
const saveBlog = (e) => {
// sending to API
console.log(blogName)
}
and in this method you change the string to an object
const handleChange = (e) => {
console.log(e.target.value)
setBlogName({
...blogName,
[e.target.name]: e.target.value
})
}
so the problem is that your function updates your state to an object and then you want to display that object(not a string property of that object) in the DOM. its wrong because you cant display objects in the DOM in react. in this case, you even get an error because you cant use spread operator on strings. you cant do something like this: ...("test")
const handleChange = (e) => {
console.log(e.target.value)
//the state value will be an object. its wrong. you even get an error
because of using spread operator on a string
setBlogName({
...blogName //this is a string at the init time,
[e.target.name]: e.target.value
})
}
so whats the solution?
you should update your state to a string or use a string property of the object.
something like this:
const handleChange = (e) => {
console.log(e.target.value)
setBlogName("string")
}
return (<>{blogName}</>)
thats it.

How can I use this React component to collect form data?

I've created two components which together create a 'progressive' style input form. The reason I've chosen this method is because the questions could change text or change order and so are being pulled into the component from an array stored in a JS file called CustomerFeedback.
So far I've been trying to add a data handler function which will be triggered when the user clicks on the 'Proceed' button. The function should collect all of the answers from all of the rendered questions and store them in an array called RawInputData. I've managed to get this to work in a hard coded version of SurveyForm using the code shown below but I've not found a way to make it dynamic enough to use alongside a SurveyQuestion component. Can anybody help me make the dataHander function collect data dynamically?
There what I have done:
https://codesandbox.io/s/angry-dew-37szi2?file=/src/InputForm.js:262-271
So, we can make it easier, you just can pass necessary data when call handler from props:
const inputRef = React.useRef();
const handleNext = () => {
props.clickHandler(props.reference, inputRef.current.value);
};
And merge it at InputForm component:
const [inputData, setInputData] = useState({});
const handler = (thisIndex) => (key, value) => {
if (thisIndex === currentIndex) {
setCurrentIndex(currentIndex + 1);
setInputData((prev) => ({
...prev,
[key]: value
}));
}
};
// ...
<Question
// ...
clickHandler={handler(question.index)}
/>
So, you wanted array (object more coninient I think), you can just save data like array if you want:
setInputData(prev => [...prev, value])
Initially, I thought you want to collect data on button clicks in the InputForm, but apparently you can do without this, this solution is simpler
UPD
Apouach which use useImperativeHandle:
If we want to trigger some logic from our child components we should create handle for this with help of forwarfRef+useImperativeHandle:
const Question = React.forwardRef((props, ref) => {
const inputRef = React.useRef();
React.useImperativeHandle(
ref,
{
getData: () => ({
key: props.reference,
value: inputRef.current.value
})
},
[]
);
After this we can save all of our ref in parent component:
const questionRefs = React.useRef(
Array.from({ length: QuestionsText.length })
);
// ...
<Question
key={question.id}
ref={(ref) => (questionRefs.current[i] = ref)}
And we can process this data when we want:
const handleComplete = () => {
setInputData(
questionRefs.current.reduce((acc, ref) => {
const { key, value } = ref.getData();
return {
...acc,
[key]: value
};
}, {})
);
};
See how ref uses here:
https://reactjs.org/docs/forwarding-refs.html
https://reactjs.org/docs/hooks-reference.html#useimperativehandle
I still strongly recommend use react-hook-form with nested forms for handle it

Visit each child in props.children and trigger a function

I want to be able to visit the children <Textfield> of my form <Form> upon submit.
In each child hook object, I also want to trigger a certain function (eg., validate_field). Not sure if this possible in hooks? I do not want to use ref/useRef and forwardRef is a blurred concept to me yet (if that's of any help).
My scenario is the form has been submitted while the user did not touch/update any of the textfields so no errors were collected yet. Upon form submit, I want each child to validate itself based on certain constraints.
I tried looking at useImperativeHandle too but looks like this will not work on props.children?
Updated working code in:
https://stackblitz.com/edit/react-ts-jfbetn
submit_form(evt){
props.children.map(child=>{
// hypothetical method i would like to trigger.
// this is what i want to achieve
child.validate_field() // this will return "is not a function" error
})
}
<Form onSubmit={(e)=>submit_form(e)}
<Textfield validations={['email']}>
<Textfield />
<Textfield />
</Form>
Form.js
function submit_form(event){
event.preventDefault();
if(props.onSubmit){
props.onSubmit()
}
}
export default function Form(props){
return (
<form onSubmit={(e)=>submit_form(e)}>
{props.children}
</form>
)
}
So the Textfield would look like this
…
const [value, setValue] = useState(null);
const [errors, setErrors) = useState([]);
function validate_field(){
let errors = []; // reset the error list
props.validations.map(validation => {
if(validation === 'email'){
if(!some_email_format_validator(value)){
errors.push('Invalid email format')
}
}
// other validations (eg., length, allowed characters, etc)
})
setErrors(errors)
}
export default function Textfield(props){
render (
<input onChange={(evt)=>setValue(evt.target.value)} />
{
errors.length > 0
? errors.map(error => {
return (
<span style={{color:'red'}}>{error}</span>
)
})
: null
}
)
}
I would recommend moving your validation logic up to the Form component and making your inputs controlled. This way you can manage the form state in the parent of the input fields and passing in their values and onChange function by mapping over your children with React.cloneElement.
I don't believe what you're trying to do will work because you are trying to map over the children prop which is not the same as mapping over say an array of instantiated child elements. That is to say they don't have state, so calling any method on them wouldn't be able to give you what you wanted.
You could use a complicated system of refs to keep the state in your child input elements, but I really don't recommend doing that as it would get hairy very fast and you can just solve the issue by moving state up to the parent.
simplified code with parent state:
const Form = ({ children }) => {
const [formState, setFormState] = useState(children.reduce((prev, curr) => ({ ...prev, [curr.inputId]: '' }), {}));
const validate = (inputValue, validator) => {}
const onSubmit = () => {
Object.entries(formState).forEach(([inputId, inputValue]) => {
validate(
inputValue,
children.filter(c => c.inputId === inputId)[0].validator
)
})
}
const setFieldValue = (value, inputId) => {
setFormState({ ...formState, [inputId]: value });
};
const childrenWithValues = children.map((child) =>
React.cloneElement(child, {
value: formState[child.inputId],
onChange: (e) => {
setFieldValue(e.target.value, child.inputId);
},
}),
);
return (
<form onSubmit={onSubmit}>
{...childrenWithValues}
</form>
)
};
const App = () =>
<Form>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="password" inputId="foo"/>
</Form>
I still don't love passing in the validator as a prop to the child, as pulling that out of filtered children is kinda jank. Might want to consider some sort of state management or pre-determined input list.

Categories

Resources