React - combine immediate input and debounced output - javascript

I have a component within a component. The parent component (Form) is expensive to render because of complex caluculations. Because of this if I hold down a key in the username input in the child component (UserForm) the page starts dropping frames (new characters don't show for a bit) because it renders the parent (which is expensive) as quickly as possible. To solve this I started storing a local instance of the username prop into state in the child component and then debouncing that state before passing it up to the parent.
The issue is when the parent component modifies the username (such as by reverting the changes) while the debounce in the child component is still in progress, the parent component's modification is overwritten by the child component's debounced output.
const UserForm = ({ value, onChange }) => {
const debouncedOnChange = useCallback(
lodash.debounce(updatedValue => {
onChange(updatedValue);
}, 500),
[onChange]
);
const [username, setUsername] = useState(value.username);
useEffect(() => {
setUsername(value.username);
}, [value.username]);
const handleUsernameOnChange = useCallback(
event => {
setUsername(event.target.value);
debouncedOnChange({
...value,
username: event.target.value,
});
},
[debouncedOnChange]
);
return (
<div>
<input value={username} onChange={handleUsernameOnChange} />
</div>
);
};
const INITIAL_STATE = {
user: {
username: 'John',
},
dog: {
name: 'Cliffard'
}
};
// <Form/> Is expensive to render because of complex calculations
const Form = () => {
const [value, setValue] = useState(INITIAL_STATE);
const [backup, setBackup] = useState(INITIAL_STATE);
return (
<form>
<UserForm
value={value.user}
onChange={updatedUserFormValue => {
setValue({
...value,
updatedUserFormValue,
});
}}
/>
<button
onClick={() => {
setValue({
...value,
user: { ...backup.user },
});
}}
>
Reset User Form
</button>
</form>
);
};

Related

Why the filter does not return the list on the initial render?

What I have is a list that was fetched from an api. This list will be filtered based on the input. But at the first render it will render nothing, unless I press space or add anything to the input. Another solution is set the fetched data to the filteredList. But I don't know if it is the right thing to set the fetched data to two arrays.
import React, { useState, useEffect } from "react";
const PersonDetail = ({ person }) => {
return (
<div>
Id: {person.id} <br />
Name: {person.name} <br />
Phone: {person.phone}
</div>
);
};
const App = () => {
const [personsList, setPersonsList] = useState([]);
const [personObj, setPersonObj] = useState({});
const [showPersonDetail, setShowPersonDetail] = useState(false);
const [newPerson, setNewPerson] = useState("");
const [filter, setFilter] = useState("");
const [filteredList, setFilteredList] = useState(personsList);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((data) => {
setPersonsList(data);
//setFilteredList(data) <-- I have to add this to work
console.log(data);
});
}, []);
const handleClick = ({ person }) => {
setPersonObj(person);
if (!showPersonDetail) {
setShowPersonDetail(!showPersonDetail);
}
};
const handleChange = (event) => {
setNewPerson(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
const tempPersonObj = {
name: newPerson,
phone: "123-456-7890",
id: personsList.length + 1,
};
setPersonsList((personsList) => [...personsList, tempPersonObj]);
//setFilteredList(personsList) <-- to render the list again when add new person
setNewPerson(" ");
};
const handleFilter = (event) => {
setFilter(event.target.value);
const filteredList =
event.target.value.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(event.target.value.toLowerCase())
)
: personsList;
setFilteredList(filteredList);
};
return (
<div>
<h2>List:</h2>
Filter{" "}
<input value={filter} onChange={handleFilter} placeholder="Enter" />
<ul>
{filteredList.map((person) => {
return (
<li key={person.id}>
{person.name} {""}
<button onClick={() => handleClick({ person })}>View</button>
</li>
);
})}
</ul>
<form onSubmit={handleSubmit}>
<input
placeholder="Add Person"
value={newPerson}
onChange={handleChange}
/>
<button type="submit">Add</button>
</form>
{showPersonDetail && <PersonDetail person={personObj} />}
</div>
);
};
export default App;
Your filtered list is actually something derived from the full persons list.
To express this, you should not create two apparently independent states in this situation.
When your asynchronous fetch completes, the filter is probably already set and you are just setting personsList which is not the list you are rendering. You are rendering filteredList which is still empty and you are not updating it anywhere, except when the filter gets changed.
To avoid all of this, you could create the filtered list on each rendering and — if you think this is not efficient enough — memoize the result.
const filteredList = useMemo(() =>
filter.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(filter.toLowerCase())
)
: personsList,
[filter, personsList]
);
When the filter input gets changed, you should just call setFilter(event.target.value).
This way, you will always have a filtered list, independent of when your asynchronous person list fetching completes or when filters get updated.
Side note: Writing const [filteredList, setFilteredList] = useState(personsList); looks nice but is the same as const [filteredList, setFilteredList] = useState([]); because the initial value will be written to the state only once, at that's when the component gets initialized. At that time personsList is just an empty array.

How to update the state based on my initial state

What I'm trying to do here is to build a dynamic component, that will be responsible to take an array of objects and the form will be built based on my formState. So I made my initial State and used .map to make the loop over the state and mapped the keys to making the label, value, and inputs appear based on the state. but my problem is at onChange. How to update the value key in every object and set the new state for it. any advice, please.
import { useState } from "react";
import InputText from "./components";
import useForm from "./hooks/useForm";
function App() {
interface formStateT {
id: number;
label: string;
value: any;
error: string;
}
const formState = [
{
id: 0,
label: "firstName",
value: "",
error: "",
},
{
id: 1,
label: "lastName",
value: "",
error: "",
},
];
const { form, validate, setForm, checkValidHandler } = useForm(formState);
const [error, setError] = useState("");
const submitFormHandler = (e: { preventDefault: () => void }) => {
e.preventDefault();
checkValidHandler();
// write form logic
// setError() will be used to take the error message
console.log(form);
};
return (
<form onSubmit={(e) => submitFormHandler(e)}>
{form.map((f: formStateT) => (
<InputText
key={f.id}
label={f.label}
value={f.value}
onChange={(e) => {
// Im trying here to update the value key of every label key.
// setForm({ ...form, [f.label.valueOf()]: f.value })
}}
valid={f.value === "" ? validate.notValid : validate.valid}
errorMsg={error === "" ? f.error : error}
classes={"class"}
/>
))}
<button>Submit</button>
</form>
);
}
export default App;
From your comment, f.value = e.target.value; is a state mutation and should be avoided, the setForm([...form]); is masking the mutation.
In App create an onChangeHandler function that takes the onChange event object and the index you want to update. Unpack the value from the onChange event and update the state. The handler should use a functional state update to update from the previous state, and create a shallow copy of the form array, using the index to shallow copy and update the correct array element.
Example:
// curried function to close over index in scope, and
// return handler function to consume event object
const onChangeHandler = index => e => {
const { value } = e.target;
setForm(form => form.map((el, i) =>
i === index
? { ...el, value }
: el
));
};
...
<form onSubmit={submitFormHandler}>
{form.map((f: formStateT, index: number) => (
<InputText
key={f.id}
label={f.label}
value={f.value}
onChange={onChangeHandler(index)} // <-- invoke and pass mapped index
valid={f.value === "" ? validate.notValid : validate.valid}
errorMsg={error === "" ? f.error : error}
classes={"class"}
/>
))}
<button>Submit</button>
</form>

React: Child State isn't updating after Parent state gets Updated

I am beginner in Reactjs. I was building an form application using the same. There I was asked to set value of input field from the server, which can be updated by user i.e. an controlled input component.
I fetched the value in parent state then I passed the value to the child state and from there I set value of input field. Now the problem arises when I update the value in parent state then the value isn't getting updated in the child state.
See the code below -
App.jsx
import { useEffect, useState } from "react";
import { Child } from "./child";
import "./styles.css";
export default function App() {
const [details, setDetails] = useState({});
useEffect(() => {
fetch("https://reqres.in/api/users/2")
.then((res) => res.json())
.then((data) => setDetails(data));
}, []);
useEffect(() => {
console.log("data of details", details?.data);
}, [details]);
return (
<div className="App">
<h1>Testing</h1>
<Child details={details} setDetails={setDetails} val={details?.data} />
</div>
);
}
Child.jsx
import { useState } from "react";
export const Child = ({ details, setDetails, val }) => {
const [value, setValue] = useState({
save: true,
...val
});
const handleChange = (e) => {
setValue({ ...value, email: e.target.value });
};
const handleSave = () => {
setDetails({
...details,
data: { ...details.data, email: value.email }
});
console.log("Data",value);
};
const handleDelete = () => {
setDetails({ ...details, data: { ...details.data, email: "" } });
console.log("Data",value);
};
return (
<div className="cont">
<input type="text" value={value.email} onChange={handleChange} />
{value.save && <button onClick={handleSave}>save</button>}
<button onClick={handleDelete}>Delete</button>
</div>
);
};
Codesandbox Link:
https://codesandbox.io/s/testing-m3mc6?file=/src/child.jsx:0-801
N.B. I have googled for solution I saw one stackoverflow question also but that wasn't helpful for me as I am using functional way of react.
Any other method of accomplishing this would be appreciated.
Try this in child component:
useEffect(()=>{
setValue({
value,
...val
});
}, [val])

Get null for innerRef of Formik

When I try to get reference on formik, I get null in current field of ref object.
const formikRef = useRef();
...
<Formik
innerRef={formikRef}
initialValues={{
title: '',
}}
onSubmit={(values) => console.log('submit')}
>
And next:
useEffect(() => {
console.log(formikRef);
}, []);
Get:
Object {
"current": Object {
"current": null,
},
}
How can I fix this problem?
Help please.
If you want to call submit function outside Formik, you can use useImperativeHandle. Document
// Children Component
const Form = forwardRef((props, ref) => {
const formik = useFormik({
initialValues,
validationSchema,
onSubmit,
...rest // other props
})
useImperativeHandle(ref, () => ({
...formik
}))
return ** Your form here **
})
and using:
// Parent Component
const Parent = () => {
const formRef = useRef(null)
const handleSubmitForm = (values, actions) => {
// handle logic submit form
}
const onSubmit = () => {
formRef.current.submitForm()
}
return (<>
<Form ref={formRef} onSubmit={handleSubmitForm} />
<button type="button" onClick={onSubmit}>Submit</button>
</>)
}
Read the ref only when it has value, and add the dependency in useEffect on the ref.
useEffect(() => {
if (formikRef.current) {
console.log(formikRef);
}
}, [formikRef]);
Remember, that refs handle it's actual value in .current property.
What worked for me was adding variables inside useEffect's [].
For my case, it was [ref.current, show].
Add an if(ref.current) {...} before any ref.current.setFieldValue in useEffect body as well or ref.current?.setFieldValue.
This cost me several hours, I hope you're better off.

How to remove the renderer of a functional memoized component that contains a child component in React?

When writing a handler for several different inputs, I ran into the problem of re-rendering components in which there are child components. How can I remove the renderer?
Only components without children and components using useMemo are not rendered.
This is only part of the code.
Here full code.
// handle changes from input
export const useInputHandler = ({ initValues }) => {
const [values, setValues] = useState(initValues || {});
const handlerChange = useCallback(
event => {
const target = event.target;
const name = target.name;
const value = target.value;
setValues({
...values,
[name]: value
});
},
[values]
);
return [values, handlerChange];
};
const App = () => {
const [clicksNum, setClicks] = useState(0);
const countClicks = useCallback(() => {
setClicks(c => c + 1);
}, []);
const [values, handleChange] = useInputHandler({
initValues: { simple: "", counter: "", empty: "" }
});
useEffect(() => {
console.log("VALUES: ", values);
}, [values, clicksNum]);
return (
<div style={{ display: "flex", flexDirection: "column", width: "30%" }}>
<Button onClick={countClicks} />
<Input onChange={handleChange} name="simple" value={values.simple}>
{<div>hello</div>}
</Input>
<Input onChange={handleChange} name="counter" value={values.counter}>
{clicksNum}
</Input>
<Input onChange={handleChange} name="empty" value={values.empty} />
</div>
);
};
I expect that the input components will not be re-render every time the button is clicked. In this case, only the second input (which name counter) should be redrawn. Because it wraps up the value (clicksNum) of the state.
Your input re-renders, because it's children change.
{<div>Hello</div>} is a different instance at every render.
If you replace this with something like this:
const hello = useMemo(() => <div>Hello</div>, []);
It will only create a new instance if any of the dependencies change. There are no dependencies, and your re-renders will be gone.
You can always prevent unwanted re-renders by memoizing any of your components, it will then re-render only if any of the dependencies change.
const memoizedInput = React.useMemo(
() => (
<Input onChange={handleChange} name="simple" value={values.simple}>
<Button onClick={countClicks} />
</Input>
),
[handleChange, countClicks, values.simple]
);

Categories

Resources