React: Updating one state property removes other states properties in the state - reactjs

I have two text boxes with id set dynamically as en which can change. Every time the onchange is emittted from input field, the state should update. The problem is, if I type something into one of the inputs, the other input state seems to disappear. For example, If I type test into title text field, the state becomes:
this.state = {
translation: {
en: {
title: 'test'
}
}
};
If I move on to typing into the content text box, it seems to replace the title state. like so,
this.state = {
translation: {
en: {
content: 'content'
}
}
};
They should update the state independently without affecting each other. Here is my intended state
this.state = {
translation: {
en: {
title: 'title-text',
content: 'content-text'
}
}
};
Component
import React from 'react';
export default class Demo extends React.Component
{
constructor(props)
{
super(props);
// default state
this.state = {
translation: {}
};
}
onSubmit(e)
{
e.preventDefault();
console.log(this.state);
}
render()
{
return (
<form onSubmit={(event) => this.onSubmit(event)}>
<input id="en" name="title"
onChange={(event) => this.setState({
translation: {
[event.target.id]: {
title: event.target.value
}
}
})} />
<input id="en" name="content"
onChange={(event) => this.setState({
translation: {
[event.target.id]: {
content: event.target.value
}
}
})} />
<button type="submit">Login</button>
</form>
);
}
}

setState does not deeply merge current state and updates. You should spread your translation state prop.
this.setState({
translation: {
...this.state.translation,
[event.target.id]: {
content: event.target.value
}
}
})

The behaviour is correct.
e.g: var obj = {a:1,b:3};
obj = {a:4};//Obj updated. Now obj.b will be undefined.This is what you worried for.
In your case, you can do something like this. It is not one of the best solution.
onSubmit(e)
{
e.preventDefault();
let state = this.state.translation;
state.en.content = 'content';
this.setState(state);
console.log(this.state);
}

As was mentioned, setState doesn't deeply merge values, however what you really need is to do is this:
this.setState({
translation: {
[event.target.id]: {
...this.state.translation[event.target.id],
content: event.target.value
}
})
And same for title.

You forgot about immutability. Just add to your code ...this.state for import all the properties that have been there before.
this.state = {
...this.state,
..
}

Related

Create a SearchBar where after a user submits the search, display all matches (or partial matches) to that title

My problem is, once I click the search button for an empty string, it should set the state for moviesSearched to movieTitles (the array of all movie titles). However, for some reason, it renders the last element in the array which in this case, is Ex Machina and I tried so many things to fix it but I just can't seem to find a solution. I'm guessing that the way I'm setting state in my filter with [movie] is wrong but I don't know what else to do.
import React from 'react';
import movies from './movieData';
const movieTitles = movies.map(movie => movie.title);
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
searchQuery: '',
searchedMovies: []
}
}
handleQuery = e => {
this.setState({
searchQuery: e.target.value
});
}
handleSearch = () => {
if (this.state.searchQuery === '') {
this.setState({ searchedMovies: movieTitles })
}
movieTitles.filter(movie => {
(movie.toLowerCase().includes(this.state.searchQuery.toLowerCase())) &&
this.setState({ searchedMovies: [movie] });
console.log(this.state.searchedMovies);
});
this.setState({
searchQuery: '',
});
}
render() {
return (
<div>
<input
type="text"
placeholder="Search for a movie.."
name="searchQuery"
value={this.state.searchQuery}
onChange={this.handleQuery}
/>
<button
onClick={this.handleSearch}
>
Search
</button>
<br />
<span>{this.state.searchedMovies}</span>
</div>
);
}
}
export default SearchBar;
var movies = [
{title: 'Mean Girls'},
{title: 'Hackers'},
{title: 'The Grey'},
{title: 'Sunshine'},
{title: 'Ex Machina'},
];
export default movies;
Issue
Well, I certainly think you've an issue with your filtering logic, after you reset the searchedMovies state back to the full movieTitles array you then run the filter. In the filter you have a side-effect that is setting the searchedMovies state to the currently iterated movie. Each iteration overwrites the previous state value. This is why you only see the last movie in the array, it was the last movie iterated!
handleSearch = () => {
if (this.state.searchQuery === '') {
this.setState({ searchedMovies: movieTitles })
}
movieTitles.filter(movie => {
(movie.toLowerCase().includes(this.state.searchQuery.toLowerCase())) &&
this.setState({ searchedMovies: [movie] }); // <-- overwrites previous update
console.log(this.state.searchedMovies);
});
this.setState({
searchQuery: '',
});
}
Solution
You should save the result of the filtering into state instead.
handleSearch = () => {
const { searchQuery } = this.state;
this.setState(
searchedMovies: movieTitles.filter(
movie => movie.toLowerCase().includes(searchQuery.toLowerCase())
),
searchQuery: '',
);
}
You are using setState wrong way. You filter inside setState like this:
this.setState({
searchedMovies: movieTitles.filter((movie) => {
return movie.toLowerCase().includes(this.state.searchQuery.toLowerCase());
}),
});

React: how do I use onSubmit to change state?

I'm quite new to React, and have only completed a few projects with it. I'm currently trying to create a form that, using onSubmit, changes the state of "isSubmitted" from false to true. When "isSubmitted" is true, I'd like to render another component to show the results of the selection.
What's currently happening is that onChange is working and I can see the value of "selectedI" set as state in the console.log when I change it. However, when I click submit, this state of "isSubmitted" doesn't change.
My code is below. Any help is greatly appreciated!
import React, { Component } from "react";
import Results from "../components/Results";
export class Create extends Component {
constructor(props) {
super(props);
this.state = {
selectedI: { value: "" },
// selectedC: { value: "" },
// selectedG: { value: "" },
// selectedA: { value: "" },
isSubmitted: false,
};
}
handleChange = (event) => {
this.setState({
selectedI: { value: event.target.value },
});
};
handleSubmit = (event) => {
event.preventdefault();
this.setState({
isSubmitted: true,
});
};
render() {
console.log(this.state);
return (
<>
<form onSubmit={this.handleSubmit} onChange={this.handleChange}>
<select value={this.state.value}>
{this.props.ingredient.map((ingredient) => {
return (
<option value={ingredient.strIngredient1}>
{ingredient.strIngredient1}
</option>
);
})}
</select>
<input type="submit" value="Submit" />
</form>
{this.state.isSubmitted && <Results />}
</>
);
}
}
export default Create;
Inside your handleSubmit method correct the case on preventdefault. It should be preventDefault. Note the capital D. Once corrected it should stop your page from reloading and resetting your state along with it. See the code below.
handleSubmit = (event) => {
event.preventDefault();
this.setState({
isSubmitted: true,
});
};

Passing the state of a Child component to a Parent?

I think I know what I need to do to make my searchLocationChange function work, but I'm not sure quite how to do it. Please forgive the indentation, was grappling a fair bit with StackOverflow's WYSIWYG!
Here's my Parent component setup:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
forecasts: [],
location: {
city: '',
country: '',
},
selectedDate: 0,
searchText: '',
};
this.handleForecastSelect = this.handleForecastSelect.bind(this);
this.searchLocationChange = this.searchLocationChange.bind(this);
}
}
With this specific function I want to make work:
searchLocationChange() {
console.log(this.state.searchText);
Axios.get('https://mcr-codes-weather.herokuapp.com/forecast', {
params: {
city: this.state.searchText,
},
})
.then((response) => {
this.setState({
forecasts: response.data.forecasts,
location: {
city: response.data.location.city,
country: response.data.location.country,
}
});
});
}
And in my Child component, the logic is:
class SearchForm extends React.Component {
constructor(props) {
super(props);
this.state = {
searchText: '',
};
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(event) {
const enteredText = event.target.value;
this.setState({
searchText: enteredText
});
}
render() {
return (
<span className="search-form">
<div className="search-form__input"><input type="text" value={this.state.searchText} onChange={this.handleInputChange} /></div>
<div className="search-form__submit"><button onClick={this.props.searchLocationChange}>Search</button></div>
</span>
);
}
}
I realise I'm trying to update the Parent component with searchText from the state of the Child component, but I can't figure out what I need to change to make this work. I have a sneaking suspicion I'm 99% of the way there, and it's only a few more lines I need, but I could be way off?
You're already passing down searchLocationChange from your parent.
in parent component:
searchLocationChange(searchedText) {
console.log(searchText);
Axios.get('https://mcr-codes-weather.herokuapp.com/forecast', {
params: {
city: searchText,
},
})
.then((response) => {
this.setState({
forecasts: response.data.forecasts,
location: {
city: response.data.location.city,
country: response.data.location.country,
},
});
});
}
in child:
render() {
const { searchText } = this.state;
return (
<span className="search-form">
<div className="search-form__input"><input type="text" value={this.state.searchText} onChange={this.handleInputChange} /></div>
<div className="search-form__submit"><button onClick={()=>{this.props.searchLocationChange(searchText)}}>Search</button></div>
</span>
);
}
You should call the function like that this.props.searchLocationChange(this.state.searchText)
You can do something like below
<div className="search-form__submit"><button onClick={() => {this.props.searchLocationChange(this.state.searchText)}}>Search</button></div>
and function definition should be
searchLocationChange(searchText) {
You are mixing controlled and uncontrolled. Either do controlled or uncontrolled. So take the search Text from parent only. Above solutiion is one way of doing this . Another way is to pass searchTxt from parent to child.
<SearchForm
searchTxt={this.state.searchTxt}
handleInputChange={this.handleInputChange}
searchLocationChange={this. searchLocationChange}
/>
Move your handleInputChange in parent:
handleInputChange = (event) => {
const enteredText = event.target.value;
this.setState({ searchText: enteredText });
}
Then change your child component respective line to
<div className="search-form__input"><input type="text" value={this.props.searchText} onChange={this.props.handleInputChange} /></div>
Now when you try the above code it should work. Now you are keeping your searchTxt in the parent component. your SearchForm component is Completely controlled now.

How to check form is valid or not in react + material?

Is there any way to know that form is valid or not in react + material ui .I am using react material in my demo .I have three field in my form all are required . I want to check on submit button that form is valid or not
Here is my code
https://codesandbox.io/s/w7w68vpjj7
I don't want to use any plugin
submitButtonHandler = () => {
console.log("error");
console.log(this.state.form);
};
render() {
const { classes } = this.props,
{ form } = this.state;
return (
<div className={classes.searchUser__block}>
<SearchForm
handleInput={this.handleInputFieldChange}
submitClick={this.submitButtonHandler}
form={form}
/>
</div>
);
}
You would have to manually do that verification if you don't want to use any library. Material-ui does not have any validation built in as per their documentation. BUT it does give you some tools for that like errorMessage to text fields for example. You just have to play with it
Example:
class PhoneField extends Component
constructor(props) {
super(props)
this.state = { errorText: '', value: props.value }
}
onChange(event) {
if (event.target.value.match(phoneRegex)) {
this.setState({ errorText: '' })
} else {
this.setState({ errorText: 'Invalid format: ###-###-####' })
}
}
render() {
return (
<TextField hintText="Phone"
floatingLabelText="Phone"
name="phone"
errorText= {this.state.errorText}
onChange={this.onChange.bind(this)}
/>
)
}
}
a bit outdated example i had laying around
Form validation can be pretty complex, so I'm pretty sure you'll end up using a library. As for now, to answer your question, we need to think about form submission flow. Here is a simple example:
"Pre-submit"
Set isSubmitting to true
Proceed to "Validation"
"Validation"
Run all field-level validations using validationRules
Are there any errors?
Yes: Abort submission. Set errors, set isSubmitting to false
No: Proceed to "Submission"
"Submission"
Proceed with running your submission handler (i.e.onSubmit or handleSubmit)
Set isSubmitting to false
And some minimal implementation would be something like:
// ...imports
import validateForm from "../helpers/validateForm";
import styles from "./styles";
import validationRules from "./validationRules";
const propTypes = {
onSubmit: PropTypes.func.isRequired,
onSubmitError: PropTypes.func.isRequired,
initialValues: PropTypes.shape({
searchValue: PropTypes.string,
circle: PropTypes.string,
searchCriteria: PropTypes.string
})
};
const defaultProps = {
initialValues: {}
};
class SearchForm extends Component {
constructor(props) {
super(props);
this.validateForm = validateForm.bind(this);
this.state = {
isSubmitting: false,
values: {
searchValue: props.initialValues.searchValue || "",
circle: props.initialValues.circle || "",
searchCriteria: props.initialValues.searchCriteria || ""
},
...this.initialErrorState
};
}
get hasErrors() {
return !!(
this.state.searchValueError ||
this.state.circleError ||
this.state.searchCriteriaError
);
}
get initialErrorState() {
return {
searchValueError: null,
circleError: null,
searchCriteriaError: null
};
}
handleBeforeSubmit = () => {
this.validate(this.onValidationSuccess);
};
validate = (onSuccess = () => {}) => {
this.clearErrors();
this.validateForm(validationRules)
.then(onSuccess)
.catch(this.onValidationError);
};
onValidationSuccess = () => {
this.setState({ isSubmitting: true });
this.props
.onSubmit(this.state.values)
.catch(this.props.onSubmitError)
.finally(() => this.setState({ isSubmitting: false }));
};
onValidationError = errors => {
this.setState({ ...errors });
};
clearErrors = () => {
this.setState({ ...this.initialErrorState });
};
updateFormValue = fieldName => event => {
this.setState(
{
values: { ...this.state.values, [fieldName]: event.target.value }
},
() => this.validate()
);
};
render() {
// ...
}
}
SearchForm.propTypes = propTypes;
SearchForm.defaultProps = defaultProps;
export default withStyles(styles)(SearchForm);
As you can see, if submission flow will grow larger (for example touching inputs, passing errors, etc), the of amount of complexity inside of a component will significantly grow as well. That is why it's more preferable to use a well-maintained library of choice. Formik is my personal preference at the moment.
Feel free to check out updated codesandbox. Hope it helps.
Hi Joy I've made desirable form validation if required fields are empty.
Here is the updated codesandbox: https://codesandbox.io/s/50kpk7ovz4

How am I switching between an uncontrolled/controlled input? React

I have a component that has an input:
state = {
org: {
orgName: ''
}
};
updateInput = field => event => {
this.setState({
org: {
[field]: event.target.value
}
})
}
render() {
let { org } = this.state
return (
<input
value={org.orgName}
onChange={this.updateInput('orgName')}
/>
)
}
I initialize the input value to '', but as soon as I type anything into the input, I get this error:
A component is changing a controlled input of type undefined to be uncontrolled
I thought if I initialized the input, then it would always be controlled, but apparently this is wrong. What is the proper way for this input to always be controlled?
Use the following format:
constructor(){
super()
this.state = {
org: {
orgName: ''
}
};
}
updateInput = (field, event) => {
this.setState({
org: {
[field]: event.target.value
}
})
}
render() {
let { org } = this.state
return (
<input
value={org.orgName}
onChange={(event) => {this.updateInput('orgName', event)}}
/>
)
}
If you use it like this then you get:
place state inside a constructor so you can change it with this.setState.
onChange is being fired only when an event is happening and not automatically fired.
You need to pass the event down through the handler chain so the handler as access to it.
not sure what you meant by updateInput = field => event => {} there is no event in the chain at that point so you cannot access it.
Hope it helps :)

Resources