Clearing value state in react - javascript

Using an api endpoint that holds the an an array of objects for specific crypto currency coins.
I created a form where users can type in a specific coin and hit submit and it will return the price. That coin will then check if its in an array of object in the api. If it's valid then I push that into the filtered results array in the constructor.
My first search query works, but when I do my second query search and hit the submit button, it fails and just reloads the page.
constructor() {
super();
this.state = {value: ''};
this.state = {coin: []};
this.state = {items: []};
this.state = {filteredResults: []};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
let coin = this.state.value;
this.findCoin(coin);
event.preventDefault();
}
findCoin(id) {
this.state.items.forEach(function(currency){
if(currency.id === id) {
this.state.filteredResults.push(currency)
}
}, this);
this.setState({filteredResults: this.state.filteredResults[0]});
}
componentDidMount() {
fetch(`https://api.coinmarketcap.com/v1/ticker/`)
.then((result)=> {
result.json()
.then(json => {
this.setState({items: json})
});
});
}
render() {
return (
<div className="App">
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
<div> Price: $ {this.state.filteredResults.price_usd}
</div>
</div>
);
}
}

The problem in this method:
findCoin(id) {
this.state.items.forEach(function(currency){
if(currency.id === id) {
this.state.filteredResults.push(currency)
}
}, this);
this.setState({filteredResults: this.state.filteredResults[0]});
}
At line
this.setState({filteredResults: this.state.filteredResults[0]});
you are setting filteredResults (which is an array) to an object and on the second search the line
this.state.filteredResults.push(currency)
gives you an error, as filredResults being a string doesn't have push method.
And as you have event.preventDefault on the last line of handleSubmit method it's not executing because of previous error and form is submitting.

This method is mutating state, which circumvents React's state checking;
findCoin(id) {
this.state.items.forEach(function(currency){
if(currency.id === id) {
this.state.filteredResults.push(currency)
}
}, this);
this.setState({filteredResults: this.state.filteredResults[0]});
}
Use a method such as filter, that gives a new array reference:
const filtered = this.state.items.filter(ccy=> ccy.id === id);
this.setState({filteredResults: filtered[0]};
Also as one of the other posters has mentioned, declare the filterResults as an object (if you are only ever going to display one filtered result), as it changes from array to object.
this.state = {filteredResults: {}};

Related

Add value from inputs to array if not exists

I am rendering in my React application an Array and for each Array I have an input element.
This elements gets as name the ID from the Array entryand a value can be entered.
I have a handleChange() functions, which gets the ID and the input value.
This data should be added to an Array, which I want to save to my State.
Later on, I have for each Array entry a Submit button, where I have to send the input value to an API endpoint.
constructor(props: Props) {
super(props);
this.state = {
sendTransferObj: []
};
this.handleChange = this.handleChange.bind(this);
}
...
handleChange(transId: string, event: React.ChangeEvent<HTMLInputElement>) {
console.log(transId, event.target.value); // returns => '123', '1'
let inputObject = [{ id: transId, value: event.target.value }]
let arrayForState = [];
const index = inputObject.findIndex(inputObject => inputObject.id != transId)
if (index === -1) {
arrayForState.push(inputObject);
} else {
console.log("object already exists")
}
console.log(arrayForState)
this.setState({
...this.state,
sendTransferObj: arrayForState
}); // error!
}
...
<Form.Control as="input" name={trans.id} onChange={this.handleChange.bind(this, trans.id)} />
My problem is currently in my handleChange(), where the input value gets saved to my arrayForObject for one input, but if I enter in a second input element a value, it gets overwritten.
Also I get various errors, when I later want to do a setState() for save this Array.
This is my whole component which I am using for reference (getting the Array that I render from my state with Redux): https://codesandbox.io/s/practical-ptolemy-69zbu?fontsize=14&theme=dark
You need to append the inputObject to the sendTransferObj array in the current state instead of appending it to a new empty array (which will cause the overwriting). You can do this using the spread syntax
handleChange(transId: string, event: React.ChangeEvent<HTMLInputElement>) {
let inputObject = [{ id: transId, value: event.target.value }]
const index = inputObject.findIndex(inputObject => inputObject.id != transId)
this.setState({
...this.state,
sendTransferObj: index === -1 ? [
...this.state.sendTransferObj,
inputObject
] : this.state.sendTransferObj
});
}
This won't update the field when the inputObject is found, though. I would recommend storing the values in an object with the ID as keys instead (and formatting it into the desired format when requesting the API). This way, you don't have to iterate through the array to find the matching object.
constructor(props: Props) {
super(props);
this.state = {
sendTransferObj: {}
};
this.handleChange = this.handleChange.bind(this);
}
...
handleChange(transId: string, event: React.ChangeEvent<HTMLInputElement>) {
this.setState({
...this.state,
sendTransferObj: {
...sendTransferObj,
[transId]: event.target.value
}
});
}

onChange handler for Object.keys().map in React to update object properties

I have a component with an empty metadata object at DOM load, the server sends data to fill the empty metadata object with properties that will be assigned values within the form. I am able to iterate through the meta data and see multiple input fields correctly labeled yet when I got to input something it either doesn't change anything and the console logs the single keystroke or it returns TypeError: Cannot read property 'handleChange' of undefined. The title field handles the change just fine.
My code:
class Item extends React.Component{
constructor(props) {
super(props);
this.state = {
title: '',
metadata: {}
}
}
componentDidMount() {
... //retrieve metadata from server
this.setState({
metadata: metadata
});
console.log(metadata); //{meta1: "", meta2: "", meta3: "", meta4: "", meta5: "", …}
}
handleChange = (field) => {
return (value) => this.setState({ [field]: value });
}
render() {
const {
title,
metafield
} = this.state;
}
return(
//code to start form
<TextField value={title} onChange={this.handleChange(title)} label="Title" type=text />
{Object.keys(metadata).map(function(key) {
return (
<TextField key={key} value={metadata[key]} onChange={this.handleChange({key})} label={key} type=text />
)
})}
//code to end form
)
}
I'm sure it's because the handleChange isn't equipped to handle changes on object properties but I'm not sure how to access that layer. I've tried binding a handleMetadataChange function on the constructor and use e.target to assign the values but the failing behavior persists.
There are a couple of bugs:
handleChange sets state like this: this.setState({ [field]: value}); but the values are in state.metadata not in state.
In render
you get metafield from state but initially you set metadata
and in handleChange you don't use any of it.
You always re create onChange for TextField even if nothing has changed, this causes needless DOM re renders.
Here is a working example:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
metadata: {},
};
}
componentDidMount() {
Promise.resolve().then(() =>
this.setState({
metadata: { x: 'x', y: 'y' },
})
);
}
handleChange = (field, value) =>
//you forgot you are setting metadata of state
this.setState({
...this.state,
metadata: { ...this.state.metadata, [field]: value },
});
render() {
const {
metadata, //you used metaField here but it's metadata
} = this.state;
return (
<div>
{Object.keys(metadata).map(key => (
<TextField
key={key}
value={metadata[key]}
onChange={this.handleChange} //always pass the same handler function
changeKey={key} //added for optimization
label={key}
/>
))}
</div>
);
}
}
//make textfield a pure component as it only receives props
// You could call this TextFieldContainer and not change TextField at all
const TextField = React.memo(function TextField({
value,
onChange,
changeKey,
label,
}) {
const rendered = React.useRef(0);
rendered.current++;
return (
<div>
times rendered: {rendered.current}
<label>
{label}
<input
type="text"
value={value}
onChange={e =>
onChange(changeKey, e.target.value)
}
/>
</label>
</div>
);
});
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Insert this at the end of your constructor: this.handleChange = this.handleChange .bind(this);
You have to be careful about the meaning of this in JSX callbacks. In JavaScript, class methods are not bound by default. If you forget to bind this.handleClick and pass it to onClick, this will be undefined when the function is actually called.
This is not React-specific behavior; it is a part of how functions work in JavaScript. Generally, if you refer to a method without () after it, such as onClick={this.handleClick}, you should bind that method
Handling Events
class Item extends React.Component{
constructor(props) {
super(props);
this.state = {
title: '',
metadata: {}
}
}
componentDidMount() {
... //retrieve metadata from server
this.setState({
metadata: metadata
});
console.log(metadata); //{meta1: "", meta2: "", meta3: "", meta4: "", meta5: "", …}
}
handleChange = (field,e) => {
let temp = this.state.metdata;
temp[field] = e.target.value;
this.setState({metadata: temp });
}
render() {
const {
title,
metafield
} = this.state;
}
return(
//code to start form
<TextField value={title} onChange={this.handleChange(title)} label="Title" type=text />
{Object.keys(metadata).map(function(key) {
return (
<TextField key={key} value={metadata[key]} onChange={(e)=>this.handleChange(e,key)} label={key} type=text />
)
})}
//code to end form
)
}

React JS state updates for collections of objects

I'm pretty new to React, and I'm trying to practice by building a simple notes app. As far as I can tell it's going great, but! I read that state should not be updated manually, so I'm copying my state array and filtering out a result for a removal operation.
But it fails! Rather, if I console log, it correctly removes the to-be-deleted element from the state array, however, when I call setState() on the copy to update my view, the list is wrong!
For some reason my React list is always removing the last element visually from the page, and appears then out of sync with my state.
The app itself is a Form container of sorts with a nested list and list-item component, which use props from the form class to manage.
What am I doing wrong?
Form Class
class NotesForm extends Component {
constructor(props) {
super(props);
const list = [
{ text: "Build out UI" },
{ text: "Add new note" },
{ text: "delete notes" },
{ text: "edit notes" }
];
this.state = {
'notes': list
};
// this.notes = list;
this.handleSubmit = this.handleSubmit.bind(this);
this.deleteNote = this.deleteNote.bind(this);
}
handleSubmit(e) {
e.preventDefault();
if (this.input.value.length === 0) { return; }
this.state.notes.push({text: this.input.value});
this.setState({ notes: this.state.notes });
this.input.value = "";
}
// BUG - deletes WRONG note!!
deleteNote(note) {
console.log({'DELETE_NOTE': note.text})
// var list = _.clone(this.state.notes);
var list = [...this.state.notes];
var filteredNotes = _.filter(list, function(n) {
return (n.text !== note.text);
})
console.log({
'list': list,
'filteredNotes': filteredNotes
})
this.setState({ notes: filteredNotes });
}
render() {
return (
<div className="row notes-form">
<div className="col-xs-12">
<form onSubmit={this.handleSubmit}>
<input type="text" className="new-note-input" ref={(input) => this.input = input} />
<br />
<button className="add-btn btn btn-info btn-block" type="button" onClick={this.handleSubmit}>Add</button>
<br />
<NotesList notes={this.state.notes} deleteNote={this.deleteNote} />
</form>
</div>
</div>
);
}
}
List Class
class NotesList extends Component {
constructor(props) {
super(props);
}
render() {
return (
<ul className="notes-list">
{this.props.notes.map((n, index) => <NotesListItem key={index} note={n} deleteNote={this.props.deleteNote} />)}
</ul>
);
}
}
List Item Class
class NotesListItem extends Component {
constructor(props) {
super(props);
this.state = {
'text': props.note.text
};
this.delete = this.delete.bind(this);
}
delete() {
this.props.deleteNote(this.props.note);
}
render() {
return (
<li className="notes-list-item">
<span className="item-text">{this.state.text}</span>
<div className="notes-btn-group btn-group" role="group">
<button className="delete-btn btn btn-danger" type="button" onClick={this.delete}>×</button>
</div>
</li>
);
}
}
Try using something like a unique id instead of index as the key for each NotesListItem in NotesList. See this related question (maybe a duplicate actually):
import React, { Component } from 'react';
import NotesListItem from './NotesListItem';
class NotesList extends Component {
constructor(props) {
super(props);
}
render() {
return (
<ul className="notes-list">
{this.props.notes.map((n, index) => <NotesListItem key={n.id} note={n} deleteNote={this.props.deleteNote} />)}
</ul>
);
}
}
export default NotesList;
You can use something like uuid to generate a "unique" id. There are many ways you could generate a unique key, but it depends on your data structure. Also using a unique id and filtering based on the id, can help avoid a situation where two notes in the array have the same text as filtering based on the text value would delete both of them.
import uuidv1 from 'uuid/v1';
// ...
handleSubmit(e) {
e.preventDefault();
if (this.input.value.length === 0) { return; }
this.state.notes.push({id: uuidv1(), text: this.input.value});
this.setState({ notes: this.state.notes });
this.input.value = "";
}
I only suggest to use something like this as it's possible your text could be duplicated. You could probably even get away with using something like:
{this.props.notes.map((n, index) => <NotesListItem key={index + n.text} note={n} deleteNote={this.props.deleteNote} />)}
Also, you shouldn't be directly mutating state like this.state.notes.push({text: this.input.value});. Try something like this instead:
handleSubmit(e) {
e.preventDefault();
if (this.input.value.length === 0) { return; }
const note = { id: uuidv1(), text: this.input.value };
const notes = [...this.state.notes, note];
this.setState({ notes });
this.input.value = "";
}
Also, I'd avoid using ref for handling controlled inputs, especially to set value. Why not create a property on state that handles the value of the input in combination with a simple onChange event handler. This would be in line with the React Forms documentation and the "standard" React way of handling input value updates:
handleChange(e) {
this.setState({ text: e.target.value });
}
handleSubmit(e) {
e.preventDefault();
if (this.state.text.length === 0) { return; }
const note = { id: uuidv1(), text: this.state.text };
const notes = [...this.state.notes, note];
this.setState({ text: '', notes });
}
render() {
// ...
<input type="text" className="new-note-input" value={this.state.text} onChange={this.handleChange} />
// ...
}
Here is an example in action.
The other answer may be enough to resolve your issue. I'd recommend to review the following article mentioned/linked in the React Keys documentation discuss the potential negative impacts of using an index as a key.
Hopefully that helps!
The constructor of a Component only runs once. React will reuse component instances passing them new props. The problem here is that NodeListItem caches the text of the note in its own local state and uses that text in the render method. When its Parent passes a new note to it via the props, it does not use it. It uses the state which is now stale.
Child components should usually use data from props passed in by the Parent.
class NotesListItem extends Component {
constructor(props) {
super(props);
// The problem is this statement here
this.state = {
'text': props.note.text
};
this.delete = this.delete.bind(this);
}
}
Here is a fixed version of the NotesListItem class.
class NotesListItem extends Component {
constructor(props) {
super(props);
this.delete = this.delete.bind(this);
}
delete() {
this.props.deleteNote(this.props.note);
}
render() {
return (
<li className="notes-list-item">
<span className="item-text">{this.props.note.text}</span> {/* <-- using props */}
<div className="notes-btn-group btn-group" role="group">
<button
className="delete-btn btn btn-danger"
type="button"
onClick={this.delete}
>
×
</button>
</div>
</li>
);
}
}

Saving the values in forms after leaving the page in reactjs

So I have a bunch of forms that the user can navigate back and forth from, but when he comes back the form should be as they left it, and all their values can be submitted at once.
I thought of keeping a large object, that temporarily stores the value for all the forms and is what is submitted by then end of it.
Whenever I return to the page, I just put that certain object in the value property.
The problem with this, is that once I return to the filled out form, I can't edit it anymore.
What can I do to make this happen? Also, I am willing to change it completely if there is a whole better way to do this.
Here is some relevant code with some comments explaining things:
// form3 only has to text fields, and form4 only has a file entry,
//which is why they aren't objects
//form1 and form2 on the other hand are huge and generate dynamically (I get
//all their fields through a get request and don't necessarily know
//how many there will be
export var formDataObject = {
form1:{},
form2:{},
form3:{
field1:"",
field2:""
},
form4:""
};
// for the next part I'll use form3 as an example since it is simple:
//The Form3 component is just a simple text input, I include the code
//of that too in case it's needed
export default class FullForm3 extends Component{
constructor(props){
super(props);
this.state ={
initial_field1: formDataObject.form3.field1,
initial_field2: formDataObject.form3.field2
}
}
render(){
var field1Value, field2Value;
if (this.state.initial_field1.length>0)
field1Value = formDataObject.form3.field1;
if (this.state.initial_field2.length>0)
field2Value = formDataObject.form3.field2;
return (
<div>
<Panel>
<Form3 controlId="field1Id" label={field1Label}
value={field1Value}/>
<br/><br/>
<Form3 controlId="field2Id" label={field2Label}
value={field2Value}/>
</div>
);
}
}
class Form3 extends Component{
constructor(props){
super(props);
this.state = {
value: formDataObject.form3,
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(e) {
const value = Object.assign({}, this.state.value);
value[e.target.id] = e.target.value;
this.setState({ value });
formDataObject.form3= this.state.value;
}
handleSubmit(e){
//things for submit (not important)
}
render(){
return (
<Form inline onSubmit={this.handleSubmit}>
<ControlLabel>{this.props.label}</ControlLabel>
<FormControl
type='text' label='Text'
value={this.props.value} onChange={this.handleChange}
/>
</FormGroup>
</Form>
)
}
}
I did it in a few steps:
Create controlled forms/inputs
OnInputChange save the formData for later use inside a callback from setState() (I used localStorage)
Get saved data in constructor(more readable in my example)/componentWillMount and setState of inputs to saved values
Now you can edit these inputs after refresh
Here you have full component with 2 inputs:
class SimpleInputs extends Component {
constructor(props) {
super(props);
if(localStorage.getItem('formData')) {
this.state = JSON.parse(localStorage.getItem('formData')); //in this case the state is just for input values
} else {
this.state = {
value1: '',
value2: ''
}
}
this.handleInput1Change = this.handleInput1Change.bind(this);
this.handleInput2Change = this.handleInput2Change.bind(this);
}
handleInput1Change(event) {
this.setState({ value1: event.target.value }, () => {
localStorage.setItem('formData', JSON.stringify(this.state));
});
}
handleInput2Change(event) {
this.setState({ value2: event.target.value }, () => {
localStorage.setItem('formData', JSON.stringify(this.state));
});
}
render() {
return(
<form>
<label>Name</label>
<input type="text" value={this.state.value1} onChange={this.handleInput1Change}/>
<label>Phone number</label>
<input type="text" value={this.state.value2} onChange={this.handleInput2Change}/>
</form>
);
}
}

How to get react to correctly render a list of removable inputs?

I'm trying to render a list of inputs in react and bind the input values to an array. I'm also trying to make it so the list items are removable. However, when I remove an item from the array, the input items are not updated how I would expect. Instead of removing the input that was removed from the middle of the array, the last input is removed and the middle input remains.
var Inputs = React.createClass({
getInitialState: function() {
return {
inputarr: ['']
};
},
render: function() {
var self = this;
return <div>{ this.state.inputarr.map(function (value, i) {
return <div key={i}><input onChange={function (e) {self.onChangeInput(i, e)}}/>
{ i < (self.state.inputarr.length - 1) && <button onClick={function () {self.onRemove(i)}}>x</button>}
</div>;
}) }</div>;
},
onChangeInput: function (i, e) {
this.state.inputarr[i] = e.target.value;
if (this.state.inputarr[this.state.inputarr.length - 1] !== '') {
this.state.inputarr.push('');
}
this.setState({
inputarr: this.state.inputarr.slice(0)
});
},
onRemove: function (i) {
this.state.inputarr.splice(i, 1);
this.setState({
inputarr: this.state.inputarr.slice(0)
});
}
});
ReactDOM.render(
<Inputs/>,
document.getElementById('container')
);
You can run this in this fiddle: https://jsfiddle.net/vvd7hex9/1/
What happens?
add something to the first input, a second will appear. Type in 3 different inputs.
remove the second input using the x button.
The last input is removed.
What I expected to happen
The middle input to be removed and only 2 inputs should contain the contents in the inputarr array.
Why does this happen? How can I fix it to remove the correct input?
Ahhhh, this is a classic javascript problem. It has to do with your map statement. You can read more about the specific details here, but what it boils down to is that when the click events actually fire, the value of i is equal to inputarr.length - 1. To fix this, you need some way of preserving the value of i during each loop. The easiest way to do this is to change the click event to this:
<button onClick={self.onRemove(i)}>x</button>
and change onRemove to this:
onRemove: function (i) {
var self = this;
return function(e) {
self.state.inputarr.splice(i, 1);
self.setState({
inputarr: this.state.inputarr.slice(0)
});
}
}
Some more info about closures can be found here if you're unfamiliar
I think it would be better to have separate Input component and App component.
Then you can create increment and decrement methods and pass them down from App to your Input components. I have build a little pen to show how you can achieve it.
I used some useful methods from lodash so take a look how them work.
https://codepen.io/dagman/pen/oYaYyL
The code itself.
class App extends React.Component {
constructor(props) {
super(props);
this.increment = this.increment.bind(this);
this.decrement = this.decrement.bind(this);
this.state = {
quantity: [0],
};
}
increment(value) {
const { quantity } = this.state;
this.setState({
quantity: quantity.concat(_.last(quantity) + 1),
});
}
decrement(el) {
const { quantity } = this.state;
this.setState({ quantity: _.without(quantity, el) })
}
render() {
const inputs = this.state.quantity.map(x => (
<Input
increment={this.increment}
decrement={this.decrement}
key={x}
toDelete={x}
/>
));
return (
<form>
{inputs}
</form>
);
}
}
class Input extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.onBtnClick = this.onBtnClick.bind(this);
this.state = {
value: '',
shouldIncrementQuantity: true,
};
}
onChange(e) {
const value = e.target.value;
this.setState({ value });
if(value.trim().length > 0 && this.state.shouldIncrementQuantity) {
this.setState({
shouldIncrementQuantity: false,
}, () => this.props.increment());
}
}
onBtnClick(e) {
e.preventDefault();
this.props.decrement(this.props.toDelete);
}
render() {
return (
<p className="input-field">
<input
type="text"
value={this.state.value}
onChange={this.onChange}
/>
<button onClick={this.onBtnClick}>x</button>
</p>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);

Categories