Problem I'm trying to solve: Form validation for an app built in React that takes in user input and generates a CV. This is for learning purposes and part of The Odin Project curriculum.
How I'm trying to solve form validation: I have my project organized with a large formData object set as state in App.js and I'm sharing that state to all child components via useContext.
For validation, I have given each piece of CV data 2 properties in my formData object. Example below:
{firstname: '', firstNameValid: true}
I am trying to write a function (see attached code) that sets the validity of each propertyValid and set it to false.
What I expect: When a field is invalid (like firstName), it sets firstNameValid: false when I run the invalidateField('firstName') function.
What happens instead: Logging the formData reveals that firstNameValid is still set to true.
What I have tried: As seen in the attatched code, I am trying to make a copy of my formData object, set only the value I want, and then just manually set it using setFormData(formCopy). However, when I log them together, I see that while formCopy looks like what I want it to be, the formData in state still has not changed. I am updating state just fine from my grandchildren components, but I'm unable to do it here and I don't understand why.
//my object declared in state
const [formData, setFormData] = React.useState({
firstName: '',
lastName: '',
email: '',
firstNameValid: true,
lastNameValid: true,
emailValid: true
//a lot more than this but you get the idea
});
//function in question that is not working
function invalidateField(string){
//takes the name of the key as a string
let key = `${string}Valid`;
let value = false;
let formCopy = {...formData};
formCopy[key] = value;
setFormData(formCopy);
console.log(formCopy, formData);
}
//function I'm writing to check validity of input fields
//called by button click elsewhere in code
function formIsValid(formData){
let validity = true;
if(formData.firstName.length < 1){ //firstName = '' (field is empty)
console.log('your first name is too short');
invalidateField('firstName');
validity = false;
}
return validity;
}
//context then passed to child components in the return statement.
The setFormData method is an async function. It takes a while until the state is updated. You console.log right after calling setFormData, thats why it looks as if your setState didnt work properly when it just needs a little more time to complete.
Above your invalidateField function you could write an useEffect to print out when your state has changed:
import { useEffect } from "react";
useEffect(() => {
console.log(formData);
}, [formData]);
This will execute the console.log as soon as formData has changed.
Related
According to the documentation of SWR: React Hooks for Data Fetching, there is an options object which can be passed in for more control of the hook.
The documentation mentions a compare option:
compare(a, b): comparison function used to detect when returned data
has changed, to avoid spurious rerenders. By default, dequal is used.
Is it something like this?
export function useUser(shouldAPIBeCalled = false) {
const { data: user, error, mutate } = useSWR(() => shouldAPIBeCalled ? '/api/user' : null, fetcher, {
compare: (_old, _new) => {
return dequal(_old, _new);
}
})
Essentially I don't want the fetcher to be called if we haven't changed the original object in my case:
If the user is still...
user: null // don't make that call again
Don't make another call because it hasn't changed!
What’s happening to me is the hook keeps firing.
I only have the hook in a Layout component to show hide navigation elements if we have a user or not,
In the submit function of the login to get the mutate function to update the user,
And in the Auth component (which wraps certain pages) to check if a user is authorized.
Thanks in advance!
I'm having trouble wording my question, so hopefully I can explain it well enough here!
I'm making a generic form component, which handles form validation on the top level. The end result would look like this:
<Form action='/api/endpoint'>
... form inputs go here
</Form>
Everything is working really well. If I go through and fill out the form, I can submit it just fine and the validation works.
The form has state which looks like this:
{
values: { name: 'Johnny', age: 18, email: 'email#example.com' },
used: { name: true, age: true, email: true },
validators: { name: (Yup Validator), age: (Yup Validator), email: (Yup Validator) }
}
The state is populated using custom onChange and onBlur functions in each of the components (they are passed using react context). For example, onBlur the used part of the state is updated to true for the current element, onChange the values part of the state is updated to the elements value, etc.
As aforementioned, this works well when I go through and use every element. What I want to happen is that when the user clicks the Submit button, it checks EVERY field to ensure it's valid before sending the POST request.
An easy solution I thought to do would be to just programmatically blur every element which calls the onBlur function. However, because setState is asynchronous, it only adds the last element to the state (because it's being overridden, if that makes sense).
I'm wondering if I can either modify how I'm setting my form state or if there's a way I can wait before setting the form state again.
I'm using functional components, and I know that useEffect exists for this purpose, but I'm not sure how to use it in my situation.
Here's how I'm setting the state onChange and onBlur:
const handleBlur = (e) => {
e.persist();
setFormState({...formState, used: {...formState.used, [e.target.name]: true}});
}
(hopefully you can infer onChange from that, it's the same but with values instead of used)
My idea of blurring everything was as follows:
const blurAll = async () => {
let inputs = [];
// Get all inputs on the current page
const inputTags = document.getElementsByTagName("input");
// Get all text areas (because they are <textarea> instead of <input>)
const textAreas = document.getElementsByTagName("textarea");
// Concat the arrays
inputs.push(...inputTags, ...textAreas);
// Trigger a blur event for each (to initialize them in formstate)
inputs.map(input => { input.focus(); input.blur(); });
}
However because setFormState uses the existing form state, it all happens too fast and so only the last element is added to the state.
I understand that this blurAll is probably not a great solution, but I was just trying to get something working (and then I was going to get only the inputs in the form itself).
This leads me to my question, how do I wait for the previous setState to complete before setting the state again (in the case on my onChange and onBlur)?
OR is there a way in JavaScript to simply update one key in my state object instead of replacing the entire object?
If you need any more information, comment and I'll provide as much as I can. I'd prefer not to use a form library, I'm trying to learn React, and this seemed like a good thing to further my knowledge! Thank you!
I have a button that submits selected values to api. Once this has been submitted I am then trying to turn button state to disable and rest the values selected back to original state before nay where selected.
This is what I am doing on upload handle:
handleStatusEditsUpload = () => {
const { value, status } = this.state;
this.setState({
value: selected,
status: {}
});
};
In my real version locally status is clearing, status is used when changing all values at the same time by clicking the header title, a dialog appears to change all values in that column.
The main one I am having trouble is with the value. Value is populated with a new array that looks at table cell and row.
Here is demo to my project: https://codesandbox.io/s/50pl0jy3xk
Why isnt the state changing? any help appreciated as always.
What is happening is that you are mutating state in your "handleValue" method.
const newValue = [...this.state.value]; // this holds reference
newValue[rowIdx][cellIdx] = val; // so that here your state is mutated ( and const "selected" with it)
In the long term you probably should change your data structure a bit, so it would be easier to merge updates in to your state value. But a quick fix would be to clone the state value before mutating it:
handleValue = (event, val, rowIdx, cellIdx) => {
const newValue = _.cloneDeep(this.state.value); // no reference anymore
newValue[rowIdx][cellIdx] = val; // update the cloned value
this.setState({
value: newValue
});
};
I just ran your code in the sandbox you provided and it's throwing errors when you click the confirm button (trying to spread non-iterable). Once that is corrected, the state updates correctly. See my fork below:
https://codesandbox.io/s/385y99575m
I've left in a few console logs so you can see the component state updating when your onClick fires.
Why are you passing in your props to the handleStatusEditsUpload method? It doesn't take an argument. Was this just part of your debugging process?
I must be missing something obvious here. I have a to-do list app which uses a function for creating new lists. Once createList is called I want to then highlight the list by setting its selected prop to true So below are the two methods for doing this. I'm trying to call one after the other. Both of them modify state using the appropriate callback that uses prevState, yet for whatever reason createList does not set the new list in state before toggleSelected gets called, and so listName is undefined in toggleSelected. Is there anyway to ensure the new list object is set in state before calling toggleSelected? I should probably be using Redux but I didn't want to get into it for my first React app.
createList = (listName) => {
const lists = {...this.state.lists};
lists[listName] = {
listName: listName,
selected: false,
todos: {}
};
this.setState(prevState => {
return {lists: prevState.lists};
});
};
toggleSelected = (listName) => {
let selected = this.state.lists[listName].selected;
selected = !selected;
this.setState(prevState => {
return {
bookLists: update(prevState.lists, {[listName]: {selected: {$set: selected}}})
};
});
};
Both methods are called in another component like so after an onSubmit handler with the new list name being passed in:
this.props.createList(newListName);
this.props.toggleSelected(newListName);
PS - If you're wondering what's up with update(), it's from an immutability-helper plugin that allows for easily setting nested values in a state object(in this case, state.lists[listName].selected)--another reason I probably should have gone with Redux.
PPS - I realize I can just set the new list's selected prop to true from the start in creatList but there's more to the app and I need to set it after creation.
Don't do what you're doing in toggleSelected right now, instead toggle the selected flag in your list (without extracting it) and then let your component know you updated the lists data by rebinding the resulting object:
class YourComponent {
...
toggleSelected(listName) {
let lists = this.state.lists;
let list = lists[listName];
list.selected = !list.selected;
this.setState({ lists });
}
..
}
Then make sure that in your render function, where you create the UI for each list, you check whether selected is true or false so you can set the appropriate classNames string.
(Also note that in your code, you used selected = !selected. That isn't going to do much, because you extracted a boolean value, flipped it, and then didn't save it back to where it can be consulted by other code)
The problem is not in the second setState function. It is at the first line of the toggleSelected() method.
When the toggleSelected() method is executed, the first setState haven't been executed.
The flow of the your code is:
createList();
toggleSelected();
setState() in createList();
setState() in toggleSelected();
Solution 1:
Use await and async keywords
Solution 2:
Use redux
I have dynamic JSON data and have a custom method to go through the JSON to dynamically render a form on the page. The reason for the JSON schema is to build various forms that is not predefined.
I have hooked up Redux so that the schema and the formValues below gets assigned as the props of this class. So, the form is rendering correctly with the correct label, correct input field types etc. When an onChange event happens on the fields, the app state(under formData) is being updated correctly. But I am noticing that when the formData changes in the app state, the entire form gets re-rendered, instead of just the "specific fields". Is this because I am storing the form values as an object under formData like this? How do I avoid this issue?
formData = {
userName: 'username',
firstName: 'firstName
}
Example schema
const form = {
"fields":[
{
"label":"Username",
"field_type":"text",
"name":"username"
},
{
"label":"First Name",
"field_type":"text",
"name":"firstName"
}
]
}
Redux state
const reducer = combineReducers({
formSchema: FormSchema,
formData: Form
});
//render method
render() {
const { fields } = this.props.form,
forms = fields.map(({label, name, field_type, required }) => {
const value = value; //assume this finds the correct value from the "formData" state.
return (
<div key={name}>
<label>{label}</label>
<input type={field_type}
onChange={this.onChange}
value={value}
name={name} />
</div>
);
})
}
//onchange method (for controlled form inputs, updates the fields in the formData app state)
onChange(event) {
this.props.dispatch(updateFormData({
field: event.target.name,
value: event.target.value
}));
}
From your example I'm not sure, but if you're rendering the whole thing in a single render() method, yes, the component will be rendered again. And that is the problem, THE component. If you are trying to have multiple components, then they should be split up as much as possible. Otherwise if the state changes, it triggers a re-render of the only component there is.
Try breaking it as much as you can.
Hints: (dont know if they apply but maybe)
use ref={}s
implement shouldComponentUpdate()
EDIT: Just thought about this, but are you storing the fields' values in your state? This doesnt feel correct. Be sure to read carefully the React guide about controlled components. (Eg try to render using plain <span>s instead of inputs, and listen to onKeyPress. Would it still work? If not you might be misusing the value attribute)