React - Autosaving blog post to sessionStorage() with controlled inputs - javascript

I am having a strange issue whilst trying to implement an auto save feature to capture my controlled inputs and save them to sessionStorage().
I have an auto-save function that runs every 30 seconds. I create an object from my input values, and then save those into sessionStorage(). I run a check to see if my created object of input values matches the currently stored object. If the new object is different, I replace the current object in sessionStorage with this new object. This seems pretty straight forward to me.
What is happening, is that I am watching the sessionStorage update one character at a time, much like how the controlled inputs I am using work when setting their values from the onChange() function. Once the object is updated fully with what I typed, it resets back to being blank.
I will show an example of the described issue with the sessionStorage below the code examples.
Here is my AddPost component, that contains the 'add post' form and the auto-save function for now:
import React, { useState, useEffect } from 'react';
//Styles
import {
AddPostContainer,
AddPostInfoInput,
AddPostInfoLabel,
AddPostTextArea,
PostOptionWrapper,
PostOptionGroup,
AddPostBtn
} from './styles';
//Components
import LivePreview from './LivePreview/LivePreview';
import Icon from '../../../Icons/Icon';
const AddPost = props => {
const [htmlString, setHtmlString] = useState('');
const [title, setTitle] = useState('');
const [postBody, setPostBody] = useState('');
const [author, setAuthor] = useState('');
const [tags, setTags] = useState('');
const [featuredImage, setFeaturedImage] = useState('');
const autoSave = async () => {
const autoSaveObject = {
title,
author,
tags,
featuredImage,
postBody
};
try {
await window.sessionStorage.setItem(
'add_post_auto_save',
JSON.stringify(autoSaveObject)
);
} catch (e) {
return;
}
};
setInterval(() => {
const currentSave = window.sessionStorage.getItem('add_post_auto_save');
const autoSaveObject = {
title,
author,
tags,
featuredImage,
postBody
};
if (currentSave === JSON.stringify(autoSaveObject)) {
return;
} else {
autoSave();
}
}, 10000);
return (
<AddPostContainer>
<AddPostInfoLabel htmlFor="title">Title</AddPostInfoLabel>
<AddPostInfoInput
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="enter post title"
id="title"
/>
<AddPostTextArea
inputwidth="100%"
height="400px"
value={postBody}
onChange={e => {
setHtmlString(e.target.value);
setPostBody(e.target.value);
}}
/>
<AddPostInfoLabel htmlFor="postbody">Live Preview:</AddPostInfoLabel>
<LivePreview id="postbody" htmlstring={htmlString} />
<PostOptionWrapper>
<PostOptionGroup width="33%">
<AddPostInfoLabel htmlFor="author">Author:</AddPostInfoLabel>
<AddPostInfoInput
type="text"
value={author}
onChange={e => setAuthor(e.target.value)}
placeholder="enter author's name"
id="author"
inputwidth="60%"
/>
</PostOptionGroup>
<PostOptionGroup width="33%">
<AddPostInfoLabel htmlFor="tags">Tags:</AddPostInfoLabel>
<AddPostInfoInput
type="text"
placeholder="enter tags separated by ,"
value={tags}
onChange={e => setTags(e.target.value)}
id="tags"
inputwidth="60%"
/>
</PostOptionGroup>
<PostOptionGroup width="33%">
<AddPostInfoLabel htmlFor="featuredImage">
Feat. Image:
</AddPostInfoLabel>
<AddPostInfoInput
type="text"
placeholder="enter image url"
value={featuredImage}
onChange={e => setFeaturedImage(e.target.value)}
id="featuredImage"
inputwidth="60%"
/>
</PostOptionGroup>
</PostOptionWrapper>
<AddPostBtn type="button">
<Icon icon={['far', 'plus-square']} size="lg" pSize="1em">
<p>Add Post</p>
</Icon>
</AddPostBtn>
</AddPostContainer>
);
};
export default AddPost;
Here is the auto-save function on it's own:
const autoSave = async () => {
const autoSaveObject = {
title,
author,
tags,
featuredImage,
postBody
};
try {
await window.sessionStorage.setItem(
'add_post_auto_save',
JSON.stringify(autoSaveObject)
);
} catch (e) {
return;
}
};
setInterval(() => {
const currentSave = window.sessionStorage.getItem('add_post_auto_save');
const autoSaveObject = {
title,
author,
tags,
featuredImage,
postBody
};
if (currentSave === JSON.stringify(autoSaveObject)) {
return;
} else {
autoSave();
}
}, 30000);
This auto-save function runs once every 30 seconds, and then replaces what is in sessionStorage with what the current values for the input fields are. I use JSON.stringify() on the objects to compare them. (note: the obj from sessionStorage is already stringified.). If that match returns true, nothing is saved as the current input values are also what is saved. Else, it saves the new object into sessionStorage.
My thought was that I needed to make autoSave() async, as updating both session and local storage is asynchronous and doesn't happen immediately (although pretty close to it). That didn't work.
Here is what the sessionStorage object is when it tries to save:
It may be a lower quality, but you can see how it is updating the 'title' property. It behaves like a controlled input, character by character being added to the value.
Can someone point out what is going on here? I am at a loss on this one. Thanks in advance!

The main issue you have is that setInterval is being called on every render and the created intervals are never being cleared.
That means that if you type 10 characters into a text input, then you'll have 10 intervals firing every 10 seconds.
To avoid this using hooks you need to wrap your setInterval call with useEffect and return a deregistration function that will clear the interval when re-rendering (or on unmount). See the Effects with Cleanup documentation.
Here is the minimal updated version using useEffect:
const autoSave = (postData) => {
try {
window.sessionStorage.setItem(
'add_post_auto_save',
JSON.stringify(postData)
);
} catch (e) {
}
};
useEffect(() => {
const intervalId = setInterval(() => {
const autoSaveObject = {
title,
author,
tags,
featuredImage,
postBody
};
const currentSave = window.sessionStorage.getItem('add_post_auto_save');
if (currentSave === JSON.stringify(autoSaveObject)) {
return;
} else {
autoSave(autoSaveObject);
}
}, 10000);
return () => {clearInterval(intervalId)};
});
If you don't want to clear and recreate the interval on every render you can conditionally control when the effect is triggered. This is covered in the useEffect Conditionally firing an effect documentation.
The main thing is that you'll need to pass in every dependency of the useEffect, which in your case is all of your state variables.
That would look like this - and you would need to make sure that you include every state variable that is used inside the useEffect hook. If you forget to list any of the variables then you would be setting stale data.
useEffect(() => {
//... your behaviour here
}, [title, author, tags, featuredImage, postBody]);
Further reading:
Here's a blog post from Dan Abramov that delves more into hooks, using setInterval as an example: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Also, you don't need to have five or six separate useState calls if it makes sense for the post data to always be "bundled" together.
You can store the post data as an object in useState instead of managing them all separately:
const [postData, setPostData] = useState({
htmlString: '',
title: '',
author: '',
tags: '',
featuredImage: '',
postBody: '',
});
function updateData(value, key) {
setPostData((prevData) => {
return {
...prevData,
[key]: value
};
});
}
const autoSave = (postData) => {
try {
window.sessionStorage.setItem(
'add_post_auto_save',
JSON.stringify(postData)
);
} catch (e) {}
};
useEffect(() => {
const intervalId = setInterval(() => {
const currentSave = window.sessionStorage.getItem('add_post_auto_save');
if (currentSave === JSON.stringify(postData)) {
return;
} else {
autoSave(postData);
}
}, 10000);
return () => {
clearInterval(intervalId)
};
}, [postData]);
// jsx:
<AddPostInfoInput
type="text"
value={postData.title}
onChange={e => updateData(e.target.value, 'title')}
placeholder="enter post title"
id="title"
/>

Good question and nice formatting too. Your problem is happening because you are creating a new interval each time your component updates, that is a lot since you are using controlled inputs. I guess you can get what you want changing your component to a class component and create the setInterval on the componentDidMount method. And don't forget to clean the interval on the component unmounting, here is an example:
import ReactDOM from "react-dom";
import React from "react";
class Todo extends React.Component {
state = {
text1: "",
text2: "",
interval: null
};
componentDidMount() {
this.setState({
interval: setInterval(() => {
const { text1, text2 } = this.state;
const autoSaveObject = {
text1,
text2
};
console.log(JSON.stringify(autoSaveObject));
}, 3000)
});
}
componentWillUnmount() {
clearInterval(this.state.interval);
}
render() {
return (
<div>
<h1>TODO LIST</h1>
<form>
<input
value={this.state.text1}
onChange={e => this.setState({ text1: e.target.value })}
/>
<input
value={this.state.text2}
onChange={e => this.setState({ text2: e.target.value })}
/>
</form>
</div>
);
}
}
ReactDOM.render(<Todo />, document.getElementById("root"));

try to useEffect rather the setInterval
as this shape
this code will make update setItem in session when the state element is change
you can get the getItem when any time you need
const [state,setState]=useState({
htmlString:"",
title:"",
postBody:"",
author:"",
tags:"",
featuredImage:""
})
useEffect(async () => {
const autoSaveObject = { ...state };
try {
await window.sessionStorage.setItem(
'add_post_auto_save',
JSON.stringify(autoSaveObject));
} catch (e) {
return console.log(e)
}
}, [state])

Related

Getting API with useEffect, inside a handle change function

I'm using the YTS API and I need to change the link for the call, I have to use
?query_term= and add the text that the user is typing, for autocomplete. I'm using mantine components for the autocomplete. I tried putting the call inside the handlechange function, but this is not possible.
const [movieNames, setMovieNames] = useState([])
const onChangeHandler = (text) => {
useEffect(() => {
const loadMovieNames = async () => {
const response = await axios.get('https://yts.mx/api/v2/list_movies.json?query_term='+text);
let arrayOfMoviesNames = [];
response.data.data.movies.forEach(i => {
arrayOfMoviesNames.push(i.title)
});
setMovieNames(arrayOfMoviesNames)
}
loadMovieNames()
}, [])
}
.
<Autocomplete
placeholder="Search Movie"
limit={8}
data={movieNames}
onChange={e => onChangeHandler(e.target.value)}
/>
You MUST use hooks in the execution context of Function Component, you used the useEffect inside a function not in the execution context of Function Component.
const YourComponent = () => {
const [movieNames, setMovieNames] = useState([]);
const loadMovieNames = async (text) => {
const response = await axios.get(
'https://yts.mx/api/v2/list_movies.json?query_term=' + text
);
let arrayOfMoviesNames = [];
response.data.data.movies.forEach((i) => {
arrayOfMoviesNames.push(i.title);
});
setMovieNames(arrayOfMoviesNames);
};
return (
<Autocomplete
placeholder="Search Movie"
limit={8}
data={movieNames}
onChange={(value) => loadMovieNames(value)}
/>
);
};
It is also possible without useEffect, so without making it so complicated by using useEffect and onChangeHandler both, only use onChangeHandler function to update the movieNames and it will automatically update the DOM texts (I mean where ever you use)...
import React, { useState } from "react";
function MoviesPage(props) {
const [ movieNames, setMovieNames ] = useState([]);
const [ searchValue, setSearchValue ] = useState("");
const onChangeHandler = async (text) => {
const response = await axios.get(
'https://yts.mx/api/v2/list_movies.json?query_term=' + text
);
let arrayOfMoviesNames = [];
response.data.data.movies.forEach(i => {
arrayOfMoviesNames.push(i.title)
});
setMovieNames(arrayOfMoviesNames);
}
return (
<div>
<Autocomplete
placeholder="Search Movie"
limit={8}
data={movieNames}
onChange={(e) => onChangeHandler(e.target.value)}
/>
</div>
);
}
export default MoviesPage;
...and just to clarify, you can use useEffect in case of API if you want to initialize the page with the API data. You can use this hook if you don't have any onChange handlers. Another way you can approach is you can update a state hook (like searchData) on the change of the Search Bar, and lastly add the the searchData variable to the useEffect dependency array:
useEffect(() => {
// use the searchData variable to populate or update the page
// ...
},
[
searchData, // <-- talking about this line
]);
So, this was my solution. Hope this helps you mate!
useEffect is a hook, which executes on state change, So keep the useEffect funtion outside the onChangeHandler and add a new state for 'query param' and setQueryState(text) inside the onChangeHandler, and put the state param as dependency in useEffect, So whenever this state gets changed this will call the use effect function automatically.

Filter data with checkbox buttons in React

I would like to filter data based on pressing multiple checkbox buttons. Currently only the most recently pressed button works and shows the output instead of also showing outputs from other buttons which are pressed as well.
The state of checkbox buttons works correctly i.e. when clicked it is true, when unclicked it is false - however I am not sure how to connect it with my find function which fetches the data.
const JobsList = (props) => {
const pageNumber = props.pageNumber || 1;
const [jobs, setJobs] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [page, setPage] = useState(pageNumber);
const [pages, setPages] = useState(1);
useEffect(() => {
const fetchJobs = async () => {
try {
retrieveJobs();
retrievePages();
pages = retrievePages();
setJobs(jobs);
setLoading(false);
} catch (error) {
console.log(error);
setLoading(false);
setError("Some error occured");
}
};
fetchJobs();
}, [page]);
const retrievePages = () => {
JobDataService.getPage(pages)
.then((response) => {
setPages(response.data.totalPages);
})
.catch((e) => {
console.log(e);
});
};
const Checkbox = ({ type = "checkbox", name, checked = false, onChange }) => {
return (
<input
type={type}
name={name}
checked={checked}
onChange={onChange}
className="btn--position"
/>
);
};
//plain object as state
const [checkedItems, setCheckedItems] = useState({}); //plain object as state
const filteredItems = [];
const handleChange = (event) => {
// updating an object instead of a Map
setCheckedItems({
...checkedItems,
[event.target.name]: event.target.checked,
filteredItems.
});
console.log("from HANDLECHANGE: ", checkedItems)
// console.log(checkedItems[event.target.checked])
// find(event.target.name)
};
useEffect(() => {
console.log("checkedItems from UseEffect: ", checkedItems);
// console.log(checkedItems)
// find(checkedItems)
}, [checkedItems]);
const checkboxes = [
{
name: "🤵‍♀️ Finance",
key: "financeKey",
label: "financeLabel",
},
{
name: "👩‍🎨 Marketing",
key: "marketingKey",
label: "marketingLabel",
},
{
name: "👨‍💼 Sales",
key: "salesKey",
label: "salesLabel",
},
{
name: "🥷 Operations",
key: "operationsKey",
label: "financeLabel",
},
{
name: "👨‍💻 Software Engineering",
key: "softwareEngineeringKey",
label: "softwareEngineeringLabel",
},
];
const retrieveJobs = () => {
JobDataService.getAll(page)
.then((response) => {
console.log(response.data);
setJobs(response.data.jobs);
})
.catch((e) => {
console.log(e);
});
};
const refreshList = () => {
retrieveJobs();
};
const find = (query, by) => {
JobDataService.find(query, by)
.then((response) => {
console.log(response.data);
setJobs(response.data.jobs);
// setPage(response.data.total_results)
setPages(response.data.totalPages);
})
.catch((e) => {
console.log(e);
});
};
return (
<div className="hero-container">
<div>
<div className="allButtons-div">
<div className="buttons-div">
<div>
<label>
{checkedItems[""]}
{/* Checked item name : {checkedItems["check-box-1"]}{" "} */}
</label>
{checkboxes.map((item) => (
<label key={item.key}>
{item.name}
<Checkbox
name={item.name}
checked={checkedItems[item.name]}
onChange={handleChange}
/>
</label>
))}
</div>
</div>
</div>
</div>
</div>
);
The function below fetches data from the MongoDB Realm database
const find = (query, by) => {
JobDataService.find(query, by)
.then((response) => {
setJobs(response.data.jobs);
setPages(response.data.totalPages);
})
.catch((e) => {
console.log(e);
});
};
To answer your question, our find() function should be a lot like your retrieveJobs() and retrievePages() functions - they interact with the data layer of your app. That said, if all we're trying to do is filter the data we already have (let's say that retrieveJobs() and retrievePages() fetches all of the jobs and pages you'll need), then we don't need refetch the data based on what's checked in your UI - we simply need to use JavaScript to filter the results by using things you should already be familiar with like map(), sort(), reduce(), filter(), etc.
To go further, this code has a lot of problems. We're using state probably a little more than we should, we're setting state in multiple places redundantly, we're using useEffect() calls that don't do much, the list goes on. I've been there - trying to do things in a "React" way can sometimes result in the opposite effect, where you're lost in endless useState() and useEffect() calls and trying to figure out where to call what event handler and why. I've gone through and made some fairly obvious changes to your code to hopefully get you on the right track to understanding what's going on a little bit better going forward, but I highly recommend going through the React docs and reading this post by Dan Abramov until you understand it (I had to read and re-read a couple paragraphs in that article over and over before it clicked, but I think it will go a long way for you).
Here's the code, it likely still has a lot of problems but best of luck moving forward!
// Since this is a constant set of data, you don't need to include it in your component; remember
// that React components are just regular old functions, so having this constant array value in your
// component means that it's being created anew every render. Let's move it above the component.
const checkboxes = [
{
name: '🤵‍♀️ Finance',
key: 'financeKey',
label: 'financeLabel',
},
{
name: '👩‍🎨 Marketing',
key: 'marketingKey',
label: 'marketingLabel',
},
{
name: '👨‍💼 Sales',
key: 'salesKey',
label: 'salesLabel',
},
{
name: '🥷 Operations',
key: 'operationsKey',
label: 'financeLabel',
},
{
name: '👨‍💻 Software Engineering',
key: 'softwareEngineeringKey',
label: 'softwareEngineeringLabel',
},
];
// the same principle applies with this smaller component. It doesn't use
// state or props from JobsList, so we should move the component outside of
// your JobsList component to make sure it's not created over and over again
// on each render; let's move it outside of JobsList
const Checkbox = ({ type = 'checkbox', name, checked = false, onChange }) => {
return (
<input
type={type}
name={name}
checked={checked}
onChange={onChange}
className="btn--position"
/>
);
};
// Since these functions seem to interact with the data layer of your app (depending on how JobDataService works of course),
// why don't we try making them functions that return a value from the data layer? Also, it looks like we're using async/await
// syntax in our useEffect call, why don't we try that here?
const retrievePages = async (pages) => {
try {
const response = await JobDataService.getPage(pages);
return response;
} catch (e) {
console.log(e);
}
};
// as an aside, I'm not sure of the difference between pages and page, but we'll keep this the same for now
const retrieveJobs = async (page) => {
try {
const response = await JobDataService.getAll(page);
return response;
} catch (e) {
console.log(e);
}
};
// to hopefully kind of answer your question, this find() function is a lot like the retrieveJobs and retrievePages functions above:
// it just interacts with your data layer - let's try and make it an async function and pull it out of the component so it can return
// results we need. As I explained above, though, if we grabbed all of our jobs and all of our pages already and just need to filter
// the data, why do we need to make a network call for that? Surely we can just use JS functions like filter(), map(), sort(), and reduce()
// to filter the results into the structures that our app needs
const find = async (query, by) => {
try {
const response = await JobDataService.find(query, by);
return response;
} catch (e) {
console.log(e);
}
};
const JobsList = (props) => {
const pageNumber = props.pageNumber || 1;
const [jobs, setJobs] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
// if pageNumber is passed as a prop, why do we need to store it in state? Presumably the parent component
// of <JobsList /> will handle keeping track of pageNumber, which is why we pass data as props. Let's comment
// out this useState call
// const [page, setPage] = useState(pageNumber);
const [pages, setPages] = useState(1);
useEffect(() => {
const fetchJobs = async () => {
try {
const jobsData = await retrieveJobs(props.page);
const pageData = await retrievePages(pages);
setJobs(jobsData);
setPages(pageData);
// why do we call retrievePages() twice? also, you've decided to store pages in state, so we'll want to use setPages
// for this instead of a normal assignment. let's comment out this assignment
// pages = retrievePages();
setLoading(false);
} catch (error) {
console.log(error);
setLoading(false);
setError('Some error occured');
}
};
fetchJobs();
}, [props.page, pages]);
const [checkedItems, setCheckedItems] = useState({});
// this is where we could do things like filter based on the checked items instead of making another network call; we have all of our data,
// we just need to do stuff with it (this is contrived but hopfully you get the idea) - every time React re-renders the JobsList component based on a new set of state or props (think something gets checked or unchecked),
// we'll just filter the data we've already fetched based on that new reality
const filteredJobs = jobs.filter((job) => job.id === checkedItems[job.id]);
const filteredPages = pages.filter((page) => page.id === checkedItems[page.id]);
const handleChange = (event) => {
// updating an object instead of a Map
setCheckedItems({
...checkedItems,
[event.target.name]: event.target.checked,
// not sure what this is, perhaps a typo; let's comment it out
// filteredItems.
});
// this find call needs two arguments, no? let's comment it out for now
// find(event.target.name)
};
// not sure of the purpose behind this second useEffect call, let's comment it out
// useEffect(() => {
// console.log("checkedItems from UseEffect: ", checkedItems);
// // console.log(checkedItems)
// // find(checkedItems)
// }, [checkedItems]);
// we'll ignore this for now as well and comment it out, we should probably be refreshing our data based on state or prop updates
// const refreshList = () => {
// retrieveJobs();
// };
return (
<div className="hero-container">
<div>
<div className="allButtons-div">
<div className="buttons-div">
<div>
<label>
{checkedItems['']}
{/* Checked item name : {checkedItems["check-box-1"]}{" "} */}
</label>
{checkboxes.map((item) => (
<label key={item.key}>
{item.name}
<Checkbox
name={item.name}
checked={checkedItems[item.name]}
onChange={handleChange}
/>
</label>
))}
</div>
</div>
</div>
</div>
</div>
);
};

Use useEffect to manage the state in React

I wrote a program that takes and displays contacts from an array, and we have an input for searching between contacts, which we type and display the result.
I used if in the search function to check if the searchKeyword changes, remember to do the filter else, it did not change, return contacts and no filter is done
I want to do this control with useEffect and I commented on the part I wrote with useEffect. Please help me to reach the solution of using useEffect. Thank you.
In fact, I want to use useEffect instead of if
I put my code in the link below
https://codesandbox.io/s/simple-child-parent-comp-forked-4qf39?file=/src/App.js:905-913
Issue
In the useEffect hook in your sandbox you aren't actually updating any state.
useEffect(()=>{
const handleFilterContact = () => {
return contacts.filter((contact) =>
contact.fullName.toLowerCase().includes(searchKeyword.toLowerCase())
);
};
return () => contacts;
},[searchKeyword]);
You are returning a value from the useEffect hook which is interpreted by React to be a hook cleanup function.
See Cleaning up an effect
Solution
Add state to MainContent to hold filtered contacts array. Pass the filtered state to the Contact component. You can use the same handleFilterContact function to compute the filtered state.
const MainContent = ({ contacts }) => {
const [searchKeyword, setSearchKeyword] = useState("");
const [filtered, setFiltered] = useState(contacts.slice());
const setValueSearch = (e) => setSearchKeyword(e.target.value);
useEffect(() => {
const handleFilterContact = () => {
if (searchKeyword.length >= 1) {
return contacts.filter((contact) =>
contact.fullName.toLowerCase().includes(searchKeyword.toLowerCase())
);
} else {
return contacts;
}
};
setFiltered(handleFilterContact());
}, [contacts, searchKeyword]);
return (
<div>
<input
placeholder="Enter a keyword to search"
onChange={setValueSearch}
/>
<Contact contacts={contacts} filter={filtered} />
</div>
);
};
Suggestion
I would recommend against storing a filtered contacts array in state since it is easily derived from the passed contacts prop and the local searchKeyword state. You can filter inline.
const MainContent = ({ contacts }) => {
const [searchKeyword, setSearchKeyword] = useState("");
const setValueSearch = (e) => setSearchKeyword(e.target.value);
const filterContact = (contact) => {
if (searchKeyword.length >= 1) {
return contact.fullName
.toLowerCase()
.includes(searchKeyword.toLowerCase());
}
return true;
};
return (
<div>
<input
placeholder="Enter a keyword to search"
onChange={setValueSearch}
/>
<Contact contacts={contacts.filter(filterContact)} />
</div>
);
};

How to change React-Hook-Form defaultValue with useEffect()?

I am creating a page for user to update personal data with React-Hook-Form.
Once paged is loaded, I use useEffect to fetch the user's current personal data and set them into default value of the form.
I put the fetched value into defaultValue of <Controller />.
However, it is just not showing in the text box.
Here is my code:
import React, {useState, useEffect, useCallback} from 'react';
import { useForm, Controller } from 'react-hook-form'
import { URL } from '../constants';
const UpdateUserData = props => {
const [userData, setUserData] = useState(null);
const { handleSubmit, control} = useForm({mode: 'onBlur'});
const fetchUserData = useCallback(async account => {
const userData = await fetch(`${URL}/user/${account}`)
.then(res=> res.json());
console.log(userData);
setUserData(userData);
}, []);
useEffect(() => {
const account = localStorage.getItem('account');
fetchUserData(account);
}, [fetchUserData])
const onSubmit = async (data) => {
// TODO
}
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>User Name:</label>
<Controller
as={<input type='text' />}
control={control}
defaultValue={userData ? userData.name : ''}
name='name'
/>
</div>
<div>
<label>Phone:</label>
<Controller
as={<input type='text' />}
control={control}
defaultValue={userData ? userData.phone : ''}
name='phone'
/>
</div>
<button>Submit</button>
</form>
</div>
);
}
export default UpdateUserData;
The called API is working well and the value is actually set to userData state.
{
name: "John",
phone: "02-98541566"
...
}
I also tried to setUserData with mock data in useEffect(), and it doesn't work either.
Is there any problem in my above code?
#tam answer is halfway through what is needed to make it work with version 6.8.3.
You need to provide the default value but also to useEffect to reset. That particular distinction is required if you have a form that you reload with another entity. I have a complete example in CodeSanbox here.
In a nutshell:
You need to define your defaultValues in the userForm.
const { register, reset, handleSubmit } = useForm({
defaultValues: useMemo(() => {
return props.user;
}, [props])
});
Then you need to listen to potential change.
useEffect(() => {
reset(props.user);
}, [props.user]);
The example in the Code Sandbox allows swapping between two users and have the form change its values.
You can use setValue (https://react-hook-form.com/api/useform/setvalue).
Import it from useForm:
const { handleSubmit, control, setValue} = useForm({ mode: 'onBlur' });
Then call it with the user data after it's received:
useEffect(() => {
if (userData) {
setValue([
{ name: userData.name },
{ phone: userData.phone }
]);
}
}, [userData]);
You can remove the default values from the form.
EDIT: See alternative answers below if this does not work.
setValue didn't work for me. Alternatively, you can use the reset method:
Reset either the entire form state or part of the form state.
Here is working code:
/* registered address */
const [registeredAddresses, setRegisteredAddresses] = useState([]);
const { register, errors, handleSubmit, reset } = useForm <FormProps> ({
validationSchema: LoginSchema,
});
/**
* get addresses data
*/
const getRegisteredAddresses = async () => {
try {
const addresses = await AddressService.getAllAddress();
setRegisteredAddresses(addresses);
setDataFetching(false);
} catch (error) {
setDataFetching(false);
}
};
useEffect(() => {
getRegisteredAddresses();
}, []);
useEffect(() => {
if (registeredAddresses) {
reset({
addressName: registeredAddresses[0].name,
tel: registeredAddresses[0].contactNumber
});
}
}, [registeredAddresses]);
Found another easy way, I used reset API from useForm
const { handleSubmit, register, reset } = useForm({ resolver });
After you call API and get back response data, you call reset with new apiData, make sure apiData key's are same as input keys (name attribute):
useEffect(() => {
reset(apiData);
}, [apiData]);
form's default values are cached and hence once you get the data from API, we reset the form state with new data.
#tommcandrew's setValue parameter formatting didn't work for me.
This format did:
useEffect(() => {
const object = localStorage.getItem('object');
setValue("name", object.name);
}, [])
although this post is 2 months old, I stumbled upon this issue today and searched for a couple of ways to do it. The most effective way I've come up with is using useMemo to set your defaultValues, like this :
const { control, errors, handleSubmit } = useForm({
reValidateMode: 'onChange',
defaultValues: useMemo(() => yourDefaultValues, [yourDefaultValues]),
});
This allows you to properly set values in your form, without the struggle of multiple implementations if you happen to have field arrays (which was my case).
This also works while using the advanced smart form component exemple from the official documentation. Let me know if you have any questions !
This works for nested objects (I'm using version 6.15.1)
useEffect(() => {
for (const [key, value] of Object.entries(data)) {
setValue(key, value, {
shouldValidate: true,
shouldDirty: true
})
}
}, [data])
Using reset is a simple solution.
const { reset } = useForm();
onClick={()=> reset({ firstname: 'Joe' }, { lastname: 'Doe' }) }
As of react-hook-form 7.41, you can use defaultValues with async functions like this:
const {
formState: { isLoading },
} = useForm({
defaultValues: fetch('API'),
// resetOptions: {
// keepDirtyValues: true
// }
});
now the defaultValue field type is look like this:
type AsyncDefaultValues<TFieldValues> = (payload?: unknown) => Promise<TFieldValues>;
isLoading for the async defaultValues loading state.

React: How to skip useEffect on first render [duplicate]

This question already has answers here:
Make React useEffect hook not run on initial render
(16 answers)
Closed last month.
I'm trying to use the useEffect hook inside a controlled form component to inform the parent component whenever the form content is changed by user and return the DTO of the form content. Here is my current attempt
const useFormInput = initialValue => {
const [value, setValue] = useState(initialValue)
const onChange = ({target}) => {
console.log("onChange")
setValue(target.value)
}
return { value, setValue, binding: { value, onChange }}
}
useFormInput.propTypes = {
initialValue: PropTypes.any
}
const DummyForm = ({dummy, onChange}) => {
const {value: foo, binding: fooBinding} = useFormInput(dummy.value)
const {value: bar, binding: barBinding} = useFormInput(dummy.value)
// This should run only after the initial render when user edits inputs
useEffect(() => {
console.log("onChange callback")
onChange({foo, bar})
}, [foo, bar])
return (
<div>
<input type="text" {...fooBinding} />
<div>{foo}</div>
<input type="text" {...barBinding} />
<div>{bar}</div>
</div>
)
}
function App() {
return (
<div className="App">
<header className="App-header">
<DummyForm dummy={{value: "Initial"}} onChange={(dummy) => console.log(dummy)} />
</header>
</div>
);
}
However, now the effect is ran on the first render, when the initial values are set during mount. How do I avoid that?
Here are the current logs of loading the page and subsequently editing both fields. I also wonder why I get that warning of missing dependency.
onChange callback
App.js:136 {foo: "Initial", bar: "Initial"}
backend.js:1 ./src/App.js
Line 118: React Hook useEffect has a missing dependency: 'onChange'. Either include it or remove the dependency array. If 'onChange' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
r # backend.js:1
printWarnings # webpackHotDevClient.js:120
handleWarnings # webpackHotDevClient.js:125
push../node_modules/react-dev-utils/webpackHotDevClient.js.connection.onmessage # webpackHotDevClient.js:190
push../node_modules/sockjs-client/lib/event/eventtarget.js.EventTarget.dispatchEvent # eventtarget.js:56
(anonymous) # main.js:282
push../node_modules/sockjs-client/lib/main.js.SockJS._transportMessage # main.js:280
push../node_modules/sockjs-client/lib/event/emitter.js.EventEmitter.emit # emitter.js:53
WebSocketTransport.ws.onmessage # websocket.js:36
App.js:99 onChange
App.js:116 onChange callback
App.js:136 {foo: "Initial1", bar: "Initial"}
App.js:99 onChange
App.js:116 onChange callback
App.js:136 {foo: "Initial1", bar: "Initial2"}
You can see this answer for an approach of how to ignore the initial render. This approach uses useRef to keep track of the first render.
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
} else {
// do things after first render
}
});
As for the warning you were getting:
React Hook useEffect has a missing dependency: 'onChange'
The trailing array in a hook invocation (useEffect(() => {}, [foo]) list the dependencies of the hook. This means if you are using a variable within the scope of the hook that can change based on changes to the component (say a property of the component) it needs to be listed there.
If you are looking for something like componentDidUpdate() without going through componentDidMount(), you can write a hook like:
export const useComponentDidMount = () => {
const ref = useRef();
useEffect(() => {
ref.current = true;
}, []);
return ref.current;
};
In your component you can use it like:
const isComponentMounted = useComponentDidMount();
useEffect(() => {
if(isComponentMounted) {
// Do something
}
}, [someValue])
In your case it will be:
const DummyForm = ({dummy, onChange}) => {
const isComponentMounted = useComponentDidMount();
const {value: foo, binding: fooBinding} = useFormInput(dummy.value)
const {value: bar, binding: barBinding} = useFormInput(dummy.value)
// This should run only after the initial render when user edits inputs
useEffect(() => {
if(isComponentMounted) {
console.log("onChange callback")
onChange({foo, bar})
}
}, [foo, bar])
return (
// code
)
}
Let me know if it helps.
I create a simple hook for this
https://stackblitz.com/edit/react-skip-first-render?file=index.js
It is based on paruchuri-p
const useSkipFirstRender = (fn, args) => {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) {
console.log('running')
return fn();
}
}, args)
useEffect(() => {
isMounted.current = true
}, [])
}
The first effect is the main one as if you were using it in your component. It will run, discover that isMounted isn't true and will just skip doing anything.
Then after the bottom useEffect is run, it will change the isMounted to true - thus when the component is forced into a re-render. It will allow the first useEffect to render normally.
It just makes a nice self-encapsulated re-usable hook. Obviously you can change the name, it's up to you.
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Example:
useEffectAfterMount(() => {
console.log("onChange callback")
onChange({foo, bar})
}, [count])
I don't understand why you need a useEffect here in the first place. Your form inputs should almost certainly be controlled input components where the current value of the form is provided as a prop and the form simply provides an onChange handler. The current values of the form should be stored in <App>, otherwise how ever will you get access to the value of the form from somewhere else in your application?
const DummyForm = ({valueOne, updateOne, valueTwo, updateTwo}) => {
return (
<div>
<input type="text" value={valueOne} onChange={updateOne} />
<div>{valueOne}</div>
<input type="text" value={valueTwo} onChange={updateTwo} />
<div>{valueTwo}</div>
</div>
)
}
function App() {
const [inputOne, setInputOne] = useState("");
const [inputTwo, setInputTwo] = useState("");
return (
<div className="App">
<header className="App-header">
<DummyForm
valueOne={inputOne}
updateOne={(e) => {
setInputOne(e.target.value);
}}
valueTwo={inputTwo}
updateTwo={(e) => {
setInputTwo(e.target.value);
}}
/>
</header>
</div>
);
}
Much cleaner, simpler, flexible, utilizes standard React patterns, and no useEffect required.

Categories