The goal I want to achieve is to implement the autosave function without hurting the performance (useless rerenders etc).
Ideally, when the autosave will happen, the state will also be updated.
I created an example component with 3 inputs, in this example the component rerenders on every keystroke. I also have a useEffect hook in which I'm looking for data changes and then I save them after 1sec.
The ChildComponent is used to preview the input data.
function App(props) {
const timer = React.useRef(null);
const [data, setData] = React.useState(props.inputData);
React.useEffect(() => {
clearTimeout(timer.current)
timer.current = setTimeout(() => {
console.log("Saving call...", data)
}, 1000)
}, [data])
const inputChangeHandler = (e, type) => {
if (type === "first") {
setData({ ...data, first: e.target.value })
} else if (type === "second") {
setData({ ...data, second: e.target.value })
} else if (type === "third") {
setData({ ...data, third: e.target.value })
}
}
return (
<>
<div className="inputFields">
<input
defaultValue={data.first}
type="text"
onChange={(e) => inputChangeHandler(e, "first")}
/>
<input
defaultValue={data.second}
type="text"
onChange={(e) => inputChangeHandler(e, "second")}
/>
<input
defaultValue={data.third}
type="text"
onChange={(e) => inputChangeHandler(e, "third")}
/>
</div>
<ChildComponent data={data} />
</>
)
}
I've read about debounce but my implementation didn't work. Has anyone run into the same problem?
Below is my debounce implementation using lodash:
React.useEffect(() => {
console.log("Saving call...", data)
}, [data])
const delayedSave = React.useCallback(_.debounce(value => setData(value), 1000), []);
const inputChangeHandler = (e, type) => {
if (type === "first") {
let obj = { ...data };
obj.first = e.target.value;
delayedSave(obj)
} else if (type === "second") {
let obj = { ...data };
obj.second = e.target.value;
delayedSave(obj)
} else if (type === "third") {
let obj = { ...data };
obj.third = e.target.value;
delayedSave(obj)
}
}
The problem with this one is that if a user types immediately (before the 1sec delay) from the first input to the second it only saves the last user input.
The problem in your implementation is that the timer is set in a closure (in useEffect) using the data your component had before the timer starts. You should start the timer once the data (or the newData in my implementation proposal) is changed. Something like:
function App(props) {
const [data, setData] = React.useState(props.inputData);
const { current } = React.useRef({ data, timer: null });
const inputChangeHandler = (e, type) => {
current.data = { ...current.data, [type]: e.target.value };
if(current.timer) clearTimeout(current.timer);
current.timer = setTimeout(() => {
current.timer = null;
setData(current.data);
console.log("Saving...", current.data);
}, 1000);
}
return (
<>
<input defaultValue={data.first} type="text" onChange={(e) => inputChangeHandler(e, "first")} />
<input defaultValue={data.second} type="text" onChange={(e) => inputChangeHandler(e, "second")} />
<input defaultValue={data.third} type="text" onChange={(e) => inputChangeHandler(e, "third")} />
</>
);
}
You should set the timer id to null inside the timer handler.
React.useEffect(()=>{
clearTimeout(timer.current)
timer.current = setTimeout(()=>{
timer.current = null;
console.log("Saving...",data)
},1000)
},[data])
Related
I have a form that has a select list of countries and a cascading select list of regions. When a new country is selected, the corresponding regions are loaded. I am trying to set the value for the region after the the select list loads as the first value in the list. On page render I am loading the country select list here:
useEffect(() => {
fetchCountries();
}, []);
When a new country is selected, I am triggering the corresponding regions to be loaded here:
useEffect(() => {
fetchRegions();
regionData.length && setInput({...input, ["region"]: regionData[0].value})
}, [input["country"]]);
This is also where I am trying to set the default region value as the first item in the list, but the state is not being updated. Where have I gone wrong? More code below, with non-relevant code removed for brevity.
export default function Signup() {
const initialInput: SignupRequest = {
country: "US",
region: ""
};
const initialErrors: ValidationError = {
country: "",
region: ""
};
const [input, setInput] = useState<SignupRequest>(initialInput);
const [errors, setErrors] = useState<ValidationError>(initialErrors);
const [countryData, setCountryData] = useState<SelectListModel["data"]>([]);
const [countryLoaded, setCountryLoaded] = useState<boolean>(false);
const [regionData, setRegionData] = useState<SelectListModel["data"]>([]);
const [regionLoaded, setRegionLoaded] = useState<boolean>(false);
useEffect(() => {
fetchCountries();
}, []);
useEffect(() => {
fetchRegions();
regionData.length && setInput({...input, ["region"]: regionData[0].value})
}, [input["country"]]);
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
const { name, value } = event.target;
setInput({ ...input, [name]: value });
}
function handleBlur(event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) {
const { name, value } = event.target;
validateInput(name, value);
}
function validateInput(name: string, value: string | string[] | boolean) {
let result = ValidationService.ValidateInput(name, value);
setErrors({ ...errors, [name]: result });
}
function handleFormSubmit(event: React.FormEvent<HTMLFormElement>) {
// process form
}
async function fetchCountries() {
const response = await LocationService.getAllCountries();
if (response?.ok) {
const responseData = await response.json() as CountryResponse[];
const countries = LocationService.maptoCountrySelectList(responseData);
setCountryData(countries);
setCountryLoaded(true);
}
}
async function fetchRegions() {
const response = await LocationService.getRegionsByCountryCode(input["country"]);
if (response?.ok) {
const responseData = await response.json() as RegionResponse[];
const regions = LocationService.maptoRegionSelectList(responseData);
setRegionData(regions);
setRegionLoaded(true);
}
}
return (
<div className="account-content">
<form onSubmit={handleFormSubmit}>
<FormGroup addClass='form-group'>
<Label label='country' title='Country' />
{!countryLoaded
? <LoadingSpinner text="Loading..." />
: <SelectList data={countryData} name='country' value={input["country"]} addClass={countryLoaded && errors["country"] ? 'is-invalid' : ''} onChange={handleSelectChange} onBlur={handleBlur} />}
<FormError message={errors["country"]} />
</FormGroup>
<FormGroup addClass='form-group'>
<Label label='region' title='State/Region/Province' />
{!regionLoaded
? <LoadingSpinner text="Loading..." />
: <SelectList data={regionData} name='region' value={input["region"]} addClass={regionLoaded && errors["region"] ? 'is-invalid' : ''} onChange={handleSelectChange} onBlur={handleBlur} />}
<FormError message={errors["region"]} />
</FormGroup>
<Button type='submit' value='Sign Up' addClass='btn-mdBlue' />
</form>
</div>
)
}
You need to setInput for region inside your fetchRegions function because setState is not synchonous so when you setRegion, and right after you setInput, region is still empty, on next render region gets filled with your previous setRegion but useEffect has already been run
const fetchRegions = async () => {
const regions = await // fetchsomething
setRegions(regions);
setInput({ ...input, region: regions[0].value });
}
I have a list and this list has several elements and I iterate over the list. For each list I display two buttons and an input field.
Now I have the following problem: as soon as I write something in a text field, the same value is also entered in the other text fields. However, I only want to change a value in one text field, so the others should not receive this value.
How can I make it so that one text field is for one element and when I write something in this text field, it is not for all the other elements as well?
import React, { useState, useEffect } from 'react'
import axios from 'axios'
function Training({ teamid }) {
const [isTrainingExisting, setIsTrainingExisting] = useState(false);
const [trainingData, setTrainingData] = useState([]);
const [addTraining, setAddTraining] = useState(false);
const [day, setDay] = useState('');
const [from, setFrom] = useState('');
const [until, setUntil] = useState('');
const getTrainingData = () => {
axios
.get(`${process.env.REACT_APP_API_URL}/team/team_training-${teamid}`,
)
.then((res) => {
if (res.status === 200) {
if (typeof res.data !== 'undefined' && res.data.length > 0) {
// the array is defined and has at least one element
setIsTrainingExisting(true)
setTrainingData(res.data)
}
else {
setIsTrainingExisting(false)
}
}
})
.catch((error) => {
//console.log(error);
});
}
useEffect(() => {
getTrainingData();
}, []);
const deleteTraining = (id) => {
axios
.delete(`${process.env.REACT_APP_API_URL}/team/delete/team_training-${teamid}`,
{ data: { trainingsid: `${id}` } })
.then((res) => {
if (res.status === 200) {
var myArray = trainingData.filter(function (obj) {
return obj.trainingsid !== id;
});
//console.log(myArray)
setTrainingData(() => [...myArray]);
}
})
.catch((error) => {
console.log(error);
});
}
const addNewTraining = () => {
setAddTraining(true);
}
const addTrainingNew = () => {
axios
.post(`${process.env.REACT_APP_API_URL}/team/add/team_training-${teamid}`,
{ von: `${from}`, bis: `${until}`, tag: `${day}` })
.then((res) => {
if (res.status === 200) {
setAddTraining(false)
const newTraining = {
trainingsid: res.data,
mannschaftsid: teamid,
von: `${from}`,
bis: `${until}`,
tag: `${day}`
}
setTrainingData(() => [...trainingData, newTraining]);
//console.log(trainingData)
}
})
.catch((error) => {
console.log(error);
});
}
const [editing, setEditing] = useState(null);
const editingTraining = (id) => {
//console.log(id)
setEditing(id);
};
const updateTraining = (trainingsid) => {
}
return (
<div>
{trainingData.map((d, i) => (
<div key={i}>
Trainingszeiten
<input class="input is-normal" type="text" key={ d.trainingsid } value={day} placeholder="Wochentag" onChange={event => setDay(event.target.value)} readOnly={false}></input>
{d.tag} - {d.von} bis {d.bis} Uhr
<button className="button is-danger" onClick={() => deleteTraining(d.trainingsid)}>Löschen</button>
{editing === d.trainingsid ? (
<button className="button is-success" onClick={() => { editingTraining(null); updateTraining(d.trainingsid); }}>Save</button>
) : (
<button className="button is-info" onClick={() => editingTraining(d.trainingsid)}>Edit</button>
)}
<br />
</div>
))}
)
}
export default Training
The reason you see all fields changing is because when you build the input elements while using .map you are probably assigning the same onChange event and using the same state value to provide the value for the input element.
You should correctly manage this information and isolate the elements from their handlers. There are several ways to efficiently manage this with help of either useReducer or some other paradigm of your choice. I will provide a simple example showing the issue vs no issue with a controlled approach,
This is what I suspect you are doing, and this will show the issue. AS you can see, here I use the val to set the value of <input/> and that happens repeatedly for both the items for which we are building the elements,
const dataSource = [{id: '1', value: 'val1'}, {id: '2', value: 'val2'}]
export default function App() {
const [val, setVal]= useState('');
const onTextChange = (event) => {
setVal(event.target.value);
}
return (
<div className="App">
{dataSource.map(x => {
return (
<div key={x.id}>
<input type="text" value={val} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
This is how you would go about it.
export default function App() {
const [data, setData]= useState(dataSource);
const onTextChange = (event) => {
const id = String(event.target.dataset.id);
const val = String(event.target.value);
const match = data.find(x => x.id === id);
const updatedItem = {...match, value: val};
if(match && val){
const updatedArrayData = [...data.filter(x => x.id !== id), updatedItem];
const sortedData = updatedArrayData.sort((a, b) => Number(a.id) - Number(b.id));
console.log(sortedData);
setData(sortedData); // sorting to retain order of elements or else they will jump around
}
}
return (
<div className="App">
{data.map(x => {
return (
<div key={x.id}>
<input data-id={x.id} type="text" value={x.value} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
What im doing here is, finding a way to map an element to its own with the help of an identifier. I have used the data-id attribute for it. I use this value again in the callback to identify the match, update it correctly and update the state again so the re render shows correct values.
Hello i have a prefilled slider which is for rating, i prefill it with data from the API and if a user slides i like to update the API Database. Everything works fine except the slider always jumps back to the prefilled value by useEffect. How can i change the state so that the value from the user takes precedence over the prefilled value ?
This is my code:
const [ user ] = useContext(Context);
const location = useLocation();
const { pathname } = location;
const splitLocation = pathname.split("/");
const movieId = splitLocation[2];
const [ MovieResults, setMovieResults ] = useState([]);
const [ value, setValue ] = useState(5);
useEffect(() => {
let isMounted = true;
fetch(`${API_URL}account/xxxxxxxx/rated/movies?api_key=${API_KEY}&session_id=${user.sessionId}&sort_by=created_at.desc`)
.then(res => res.json())
.then(data => isMounted ? setMovieResults(data.results) : null)
.then(isMounted ? MovieResults.forEach((arrayItem) => {
if(arrayItem.id.toString() === movieId.toString()) {
setValue(arrayItem.rating);
}
}) : null)
return () => { isMounted = false };
},[user.sessionId, MovieResults, movieId]);
const changeSlider = (e) => {
// This value should override the default value from the api
setValue(e.currentTarget.value);
};
return (
<div>
<input
type="range"
min="1"
max="10"
className="rate-slider"
value={value}
onChange={e => changeSlider(e)}
/>
{value}
<p>
<button className="rate-button" onClick={() => callback(value)}>Rate</button>
</p>
</div>
)
Thanks help is appreciated.
I finally managed to solve it. I needed to give one of the two setValue precedence over the other based on the condition that the user slided the slider. Up to now i didnt know useRef, with this hook i could manage it. Here is the code:
const [ user ] = useContext(Context);
const location = useLocation();
const { pathname } = location;
const splitLocation = pathname.split("/");
const movieId = splitLocation[2];
const [ MovieResults, setMovieResults ] = useState([]);
const [ value, setValue ] = useState(5);
const ref = useRef(0);
useEffect(() => {
let isMounted = true;
fetch(`${API_URL}account/jensgeffken/rated/movies?api_key=${API_KEY}&session_id=${user.sessionId}&sort_by=created_at.desc`)
.then(res => res.json())
.then(data => isMounted ? setMovieResults(data.results) : null)
.then(isMounted ? MovieResults.forEach((arrayItem) => {
if(arrayItem.id.toString() === movieId.toString()) {
//console.log(ref.current)
if(ref.current === 0) {
setValue(arrayItem.rating);
}
}
}) : null)
return () => { isMounted = false };
},[user.sessionId, MovieResults, movieId]);
const handleSlide = (e) => {
ref.current = e.currentTarget.value
setValue(ref.current);
}
return (
<div>
<input
type="range"
min="1"
max="10"
className="rate-slider"
value={value}
onChange={e => handleSlide(e)}
/>
{value}
<p>
<button className="rate-button" onClick={() => callback(value)}>Rate</button>
</p>
</div>
)
Following your general approach, this seems to work fine for me:
const fetch = () => new Promise(resolve => setTimeout(() =>
resolve({json: () => ({rating: 4})})
, 1000));
const App = () => {
const [sliderVal, setSliderVal] = React.useState(0);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
fetch("foo")
.then(res => res.json())
.then(data => {
setLoading(false);
setSliderVal(data.rating);
});
}, []);
const handleSliderChange = evt => {
setSliderVal(evt.target.value);
};
return (
<div>
{loading
? <p>loading...</p>
: <div>
<input
type="range"
min="1"
max="10"
value={sliderVal}
onChange={handleSliderChange}
/>
{sliderVal}
</div>
}
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
I am having a slight issue with my react code. I want the data to be completely fetched before rendering. However, I have tried various ways such as 'setGroupLoaded to true' after the async call, but it is still not working. When the component first mounts, 'groupLoaded == false and myGroup == [],', then it goes to the next conditional statement where 'groupLoaded == true' but the myGroup [] is still empty. I was expecting the myGroup [] to be filled with data since groupLoaded is true. Please I need help with it.
function CreateGroup({ currentUser }) {
const [name, setName] = useState("");
const [myGroup, setMyGroup] = useState([]);
const [groupAdded, setGroupAdded] = useState(false);
const [groupLoaded, setGroupLoaded] = useState(false);
const handleChange = (e) => {
const { value, name } = e.target;
setName({
[name]: value,
});
};
useEffect(() => {
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
fetchGroupCreated();
setGroupLoaded(true);
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
useEffect(() => {
let groupId = myGroup.length ? myGroup[0].id : "";
let groupName = myGroup.length ? myGroup[0].groupName : "";
if (myGroup.length) {
JoinGroup(currentUser, groupId, groupName);
setTimeout(() => fetchGroupMembers(), 3000);
}
}, [myGroup]);
let itemsToRender;
if (groupLoaded && myGroup.length) {
itemsToRender = myGroup.map((item) => {
return <Group key={item.id} item={item} deleteGroup={deleteGroup} />;
});
} else if (groupLoaded && myGroup.length === 0) {
itemsToRender = (
<div>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="exampleInputTitle">Group Name</label>
<input
type="text"
className="form-control"
name="name"
id="name"
aria-describedby="TitleHelp"
onChange={handleChange}
/>
</div>
<button type="submit" className="btn btn-primary">
Add group{" "}
</button>
</form>
</div>
);
}
return (
<div>
<h3>{currentUser ? currentUser.displayName : ""}</h3>
{itemsToRender}
</div>
);
}
The problem is here:
useEffect(() => {
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
fetchGroupCreated();
setGroupLoaded(true);
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
When you call setGroupLoaded(true), the first call to setMyGroup(groupArr) hasn't happened yet because fetchMyGroup(currentUser) is asynchronous. If you've never done this before, I highly recommend putting in some logging statements, to see the order in which is executes:
useEffect(() => {
const fetchGroupCreated = async () => {
console.log("Got data")
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
console.log("Before starting to load")
fetchGroupCreated();
setGroupLoaded(true);
console.log("After starting to load")
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
The output will be:
Before starting to load
After starting to load
Got data
This is probably not the order you expected, but explains exactly why you get groupLoaded == true, but the myGroup is still empty.
The solution (as Nick commented) is to move the setGroupLoaded(true) into the callback, so that it runs after the data is retrieved:
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
setGroupLoaded(true);
};
fetchGroupCreated();
An alternative approach may be to await the call to fetchGroupCreated(). I'm not sure if it'll work, but if it does it'll be simpler:
await fetchGroupCreated();
I want to get latest state after updating state.
So I need to use useEffect.
After I change content of todo, I call saveEditedTodo onBlur.
So my code is,
useEffect(() => {
console.log(todos)
// I need to setTodos(todos), but it causes infinite loop
}, [todos]);
const saveEditedTodo = (e, id) => {
const newContent = e.currentTarget.innerHTML;
const editedTodo = todos.map((todo) =>
todo.id === id ? { ...todo, todoItem: newContent } : todo,
);
setTodos(editedTodo); // Re-rendering
onBlur(todos); // Re-rendering
};
And onBlur from props is,
const handleOnBlurTodo = (value) => {
const newValue = convertTodoToNote(value);
setEditableNote({ ...editableNote, content: newValue });
};
How can I get latest state using useEffect?
(+) Here is my full code!
function TodoList({ todoContent, onBlur }) {
const [todos, setTodos] = useState(todoContent);
const [isHover, setIsHover] = useState({ hoverID: '', onHover: false });
const { hoverID, onHover } = isHover;
const isEditable = useSelector((state) => state.isSelected);
const doneTodo = todos ? todos.filter((todo) => todo.isDone).length : 0;
useEffect(() => {
console.log(todos);
}, [todos]);
const saveEditedTodo = (e, id) => {
const newContent = e.currentTarget.innerHTML;
const editedTodo = todos.map((todo) =>
todo.id === id ? { ...todo, todoItem: newContent } : todo,
);
setTodos(editedTodo); // Re-rendering
onBlur(todos); // Re-rendering
};
const handleDeleteTodo = (id) => {
let newTodos = todos.filter((el) => el.id !== id);
setTodos(newTodos);
onBlur(todos);
};
const handleOnMouseOver = (id) => {
setIsHover({ hoverID: id, onHover: true });
};
const handleOnMouseLeave = (id) => {
setIsHover({ hoverID: id, onHover: false });
};
const handleCheckbox = (id) => {
const newTodos = todos.map((todo) =>
todo.id === id ? { ...todo, isDone: !todo.isDone } : todo,
);
setTodos(newTodos);
console.log('[todos]' + todos);
};
const todoTask = todos.filter((todo) => !todo.isDone);
const doneTask = todos.filter((todo) => todo.isDone);
if (isEditable && todos) {
let todoList = todoTask.map((todo, i) => (
<TodoListContainer
key={i}
onMouseEnter={() => handleOnMouseOver(todo.id)}
onMouseLeave={() => handleOnMouseLeave(todo.id)}
>
<Checkbox
type="checkbox"
checked={todo.isDone}
onChange={() => handleCheckbox(todo.id)}
/>
<NoteTitle
isTodoItem
size="medium"
placeholder="Add Todo"
onBlur={(e) => saveEditedTodo(e, todo.id)}
contentEditable
suppressContentEditableWarning="true"
>
{todo.todoItem}
</NoteTitle>
{hoverID === todo.id && onHover && (
<Tool
title="Delete Todo"
bgImage={DeleteIcon}
deleteTodo={() => handleDeleteTodo(todo.id)}
/>
)}
</TodoListContainer>
));
let doneList = doneTask.map((todo, i) => (
<TodoListContainer
key={i}
onMouseEnter={() => handleOnMouseOver(todo.id)}
onMouseLeave={() => handleOnMouseLeave(todo.id)}
>
<Checkbox
type="checkbox"
onBlur={() => handleCheckbox(todo.id)}
checked={todo.isDone}
/>
<NoteTitle
isTodoItem
size="medium"
placeholder="Add Todo"
onInput={(e) => saveEditedTodo(e, todo.id)}
contentEditable
suppressContentEditableWarning="true"
>
{todo.todoItem}
</NoteTitle>
{hoverID === todo.id && onHover && (
<Tool
title="Delete Todo"
bgImage={DeleteIcon}
deleteTodo={() => handleDeleteTodo(todo.id)}
/>
)}
</TodoListContainer>
));
return (todoList = (
<div>
{todoList}
{doneTodo > 0 && <CompletedTodo doneTodo={doneTodo} />}
{doneList}
</div>
));
}
if (!isEditable && todos) {
const todoList = todos.map((todo, i) => (
<TodoListContainer key={i}>
<Checkbox
type="checkbox"
onChange={() => handleCheckbox(todo.id)}
checked={todo.isDone}
/>
<NoteTitle size="small">{todo.todoItem}</NoteTitle>
</TodoListContainer>
));
return todoList;
}
return null;
}
export default TodoList;
Generally React.useEffect() is used for performing side effects for a React component. What I believe is that you wish to get the new state rendered on screen after saving the TODO content, and that can be just achieved by an onChange handler wherever you are receiving the input for your todos.
<TextField onChange={(e) => saveEditedTodos(e, id)} />
This will trigger the saveEditedTodos callback every time the value of the TextField changes. If you want to trigger the callback on clicking a save button, you can add an onClick handler in the Button component.
Another scenario what I can imagine is that you're saving your TODOs somewhere, so you want to update the list on the screen after saving the TODO in some storage, in that case you can fetch the value of todoList on each save. This can be done inside a useEffect hook callback.
React.useEffect(() => {
fetchTodos().then((response) => setTodos(response.data))
})
Here fetchTodos() is a JS Promise or async function which fetches the updated state of TODOs and sets the received data using setTodos