React form behaving strange with this.handleChange and Axios - reactjs

SOLVED
I have a React/Typescript app which has a form that I'm using in a website that renders movies we've seen from a Mongo database. I have it set up so that we can edit one of the movies in the database through an edit form. Everything goes very smoothly but for the first field which is the movie title. Whenever I change that field it immediately stops me from doing anything else and I have to click the field again.
I.E. initial title: "Foo" -> "Fo" -> click -> "F" -> click -> "" -> click -> "B" -> click -> "Ba" -> click -> "Bar".
All other fields in the don't have this behavior. As far as I can tell all input forms are the same (type: text) and there should not be any difference. I can type full sentences without a pause in any other input field but no matter what I do, the first one doesn't allow me to. Why?
const EditBody = styled.div`{blablabla}`;
export interface Props {
match: {
params: {
title: string
}
},
history: any
}
type MovieObject = {
comments: string,
imdb: string,
mtc: string,
rtc: string,
smn: string,
title: string
}
class Edit extends Component<Props, { movie: any }> {
constructor(props: any) {
super(props);
this.state = {
movie: [
{
comments: '',
imdb: '',
mtc: '',
rtc: '',
smn: '',
title: '',
pictureUrl: '',
__v: Number,
_id: String
}]
};
this.handleChange = this.handleChange.bind(this);
};
componentDidMount() {
this.callServerForDatabase()
.then(data => this.setState({ movie: data }))
.catch(err => console.log(err));
};
callServerForDatabase = async () => {
const { match: { params } } = this.props;
const response = await fetch(`/edit/${params.title}`);
const body = await response.json();
if (response.status !== 200) throw Error(body.message);
return body;
};
handleChange(e: any) {
e.preventDefault();
const name = e.target.name;
const value = e.target.value;
let thisStateMovieClone: MovieObject[] = [...this.state.movie];
let updatedMovieObj: MovieObject = {
...thisStateMovieClone[0],
[name]: value
}
thisStateMovieClone[0] = updatedMovieObj;
this.setState({
movie: thisStateMovieClone
})
}
handleUpdate = (e: any) => {
e.preventDefault();
axios.post(`/edit/${this.state.movie.title}`, this.state.movie)
.then(() => this.props.history.push("/database"));
};
handleDelete = (e: any) => {
e.preventDefault();
axios.post('/delete', this.state.movie)
.then(() => this.props.history.push("/database"));
};
render() {
return (
<EditBody>
<h2>Update Movie Data</h2>
{this.state.movie.map((movie: any) =>
<div key={movie.title} className="container">
<form action={`/edit/${movie.title}?_method=PUT`} method="PUT">
<div className="inputrow">
<p className="description">Movie Title
<input className="input" type="text" value={movie.title} onChange={this.handleChange} name="title" /></p></div>
<div className="inputrow">
<p className="description">Rotten Tomatoes Score
<input className="input" type="text" value={movie.rtc} onChange={this.handleChange} name="rtc" /></p></div>
<div className="inputrow">
<p className="description">Metacritic Score
<input className="input" type="text" value={movie.mtc} onChange={this.handleChange} name="mtc" /></p></div>
<div className="inputrow">
<p className="description">IMDB Score
<input className="input" type="text" value={movie.imdb} onChange={this.handleChange} name="imdb" /></p></div>
<div className="inputrow">
<p className="description">Scary Movie Night Score
<input className="input" type="text" value={movie.smn} onChange={this.handleChange} name="smn" /></p></div>
<div className="inputrow">
<p className="description">Comments
<input className="input" type="text" value={movie.comments} onChange={this.handleChange} name="comments" /></p></div>
<button id="submitButton" type="submit" onClick={this.handleUpdate}><h2>Update Movie</h2></button> or <button id="submitButton" type="submit" onClick={this.handleDelete}><h2>Delete Movie</h2></button>
</form>
</div>
)}
</EditBody>
)
}
};
On top of that I have a problem with the axios request to the server. It works (i.e. the movie gets updated or deleted from the DB) but the this.props.history.push either doesn't work (right now) or when it does, it's faster than the server response so it re-routes back to a /database page which hasn't received the updated information yet. Only after a refresh does /database show the correct information. I've tried async/await, setTimeout, this.props.context.push, and as you an see a .then() after the returned promise. Nothing works. I think because this.history pushes to an old version of the page, instead of actually loading the page again?
Both these problems are driving me nuts! Any help would be appreciated. If someone needs to see more code let me know.
EDIT:
The componentDidMount() and callServer.. functions are because we get to this /edit page by clicking on a movie that we want to edit. So it gets the information for that movie from the server after I click it on another page.

For anyone reading: it was because I had the {movie.title} also as the key of the container div. This meant it re-rendered the div each time, since the key also changed. I created an index parameter and assigned the key to that which solves the rendering problem. Now just the axios problem left..
{this.state.movie.map((movie: any, index: any) =>
<div key={index} className="container"> //CHANGED KEY VALUE
<form action={`/edit/${movie.title}?_method=PUT`} method="PUT">
// WHICH COLLIDED WITH THIS VALUE
<div className="inputrow">
<p className="description">Movie Title
<input className="input" type="text" value={movie.title} onChange={this.handleChange} name="title" /></p></div>

And also solved the axios problem. Updated through this.setState and added a conditional in the render function. Now it works with . For anyone happening upon this question.

Related

React UseState hook for updating data - CRUD

Im just wondering if anyone could point out where im going wrong with my code.
Im relativly new to react so began with a simple todo list.
I then edited this to allow for various other forms such as menu, profile etc.
Below is the code attached for the menu section.
My back end works if I use postmaster which leads me to believe its my front end, and specifically my useState.
I can call the data and view it within my modal and it appears, however, I cant seem to edit the specific data within the form field and/or post it to my database.
Any help would be greatly appreciated.
Ive attached my code below.
import React, { Fragment, useState } from "react";
const EditMenu = ({ menu }) => {
//editText function
const [inputs, setInputs] = useState(menu.item_title, menu.item_price, menu.item_description, menu.item_category);
const { title, category, price, description } = inputs;
const onChange = e =>
setInputs({ ...inputs, [e.target.name]: e.target.value });
const editMenuItem = async (item_id) => {
try {
const body = { title, category, price, description };
const res = await fetch(`http://localhost:5000/menu/${item_id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
window.location = "/admin";
} catch (error) {
console.error(error.message);
}
};
return (
<Fragment>
<button type="button" className="btn btn-warning" data-toggle="modal" data-target={`#id${menu.item_id}`}>Edit</button>
{/*id = "id21"*/}
<div className="modal" id={`id${menu.item_id}`} onClick={() => setInputs(menu.item_title, menu.item_price, menu.item_description, menu.item_category)}>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">Edit Menu</h4>
<button className="close btn-danger" data-dismiss="modal" onClick={() => setInputs(menu.item_title, menu.item_price, menu.item_description, menu.item_category)}>×</button>
</div>
<div className="modal-body">
<input type="text" name="title" placeholder="Title" className="form-control my-3" value={menu.item_title} onChange={e => onChange(e)} />
<input type="tel" name="price" placeholder="Price" className="form-control my-3" value={menu.item_price} onChange={e => onChange(e)} />
<input type="text" name="description" placeholder="Description" className="form-control my-3" value={menu.item_description} onChange={e => onChange(e)} />
<input type="text" name="category" placeholder="Category" className="form-control my-3" value={menu.item_category} onChange={e => onChange(e)} />
</div>
<div className="modal-footer">
<button type="button" className="btn btn-warning" data-dismiss="modal" onClick={() => editMenuItem(menu.item_id)}>Edit</button>
<button type="button" className="btn btn-danger" data-dismiss="modal" onClick={() => setInputs(menu.item_title, menu.item_price, menu.item_description, menu.item_category)}>Close</button>
</div>
</div>
</div>
</div>
</Fragment>
);
};
Update,
Ive tried various suggested fixes using the below answers so far.
Both of these fixes allow the form fields to be editable, and the information within the form fields changes and thus within the state also however it is not sent to the database. Upon refresh of the page, the old information is pulled from the database.
Ive discovered that if I removed all of the form fields but one, it successfully updates AND sends to the database.
Title OR Description OR Price OR Category.
Checking the network tab within the browser whilst updating shows that for more than one input field, the put request fails and no information/payload is sent to the body within the request tab.
As a result, the database returns a NOT NULL error.
Based off Oliviers answer below, that setInput is only recognises one parameter, I can only imagine that this is what is breaking when there is more than one form field/input added. I unfortunatly dont know enough react to know if this is the case or not.
I see a problem in your state initialization => const [inputs, setInputs] = useState(menu.item_title, menu.item_price, menu.item_description, menu.item_category); is not correct, useState take a single parameter, here you must build an object representing the inputs.
Here is a solution using a function to initialize the inputs state, to prevent computing the object each time the component is re-rendered
function buildInputs(menu) {
return {
title: menu.item_title,
category: menu.item_category,
price: menu.item_price,
description: menu.item_description
};
}
const EditMenu = ({ menu }) => {
//editText function
const [inputs, setInputs] = useState(() => buildInputs(menu));
const { title, category, price, description } = inputs;
// Needed if you want the inputs to be updtated when the menu property is updated
useEffect(() => setInputs(buildInputs(menu)), [menu]);
const onChange = e => setInputs({ ...inputs, [e.target.name]: e.target.value });
...
You must also change the input value to reflect the state variable :
<input type="text" name="title" placeholder="Title"
className="form-control my-3" value={title} onChange={onChange} />
You should set your state like this:
const [inputs, setInputs] = useState({
title: menu.item_title,
price: menu.item_price,
category: menu.item_category,
description: menu.item_description
});
also you need to change value attributes to be variables rather than setting them to the menu values, for example:
//code
<input name="title" value={inputs.title} onChange={onChange}/>
cause values inside inputs are changeable by your onChange method, on the other hand, values inside menu object will remain with the same values.
I eventually figured out the issue.
By splitting my setInput useState into seperate individual useStates, I was able to get it to work.
So my origional code of...
const EditMenu = ({ menu }) => {
const [inputs, setInputs] = useState(menu.item_title, menu.item_price, menu.item_description, menu.item_category);
const { title, category, price, description } = inputs;
changed to this.
const EditMenu = ({ menu }) => {
const [item_title, setTitle] = useState(menu.item_title);
const [item_price, setPrice] = useState(menu.item_price);
const [item_description, setDescription] = useState(menu.item_description);
and the onChange function and form input...
const onChange = e =>
setInputs({ ...inputs, [e.target.name]: e.target.value });
<input... onChange={e => onChange(e)} />
changed to this...
value={item_title} onChange={e => setTitle(e.target.value)} />
value={item_price} onChange={e => setPrice(e.target.value)} />
value={item_description} onChange={e => setDescription(e.target.value)} />
In the end, Oliviers reasoing was correct even if the soloution didnt work for me. That my setInput only allowed for one parameter. Splitting it up allowed me to pass the remaining parameters.
Thank you everyone for the help, hopefully this might help someone else some day too!

System throws exception and display playerProfile.map is not a function

I would like to display data received in the Player Profile fields. Started with name field, it displays name data, but while trying to edit the name text field, system throws following exception, TypeError: playerProfile.map is not a function. I have wrapped the fetch call inside the arrow function. Can someone please advise on what is the root cause of this error.
Note: At the moment I have received the value for name field only, need to display for other fields and still need to work on handleSubmit()
Detailed error message from console:
Uncaught TypeError: playerProfile.map is not a function
at Profile (Profile.js:34)
at renderWithHooks (react-dom.development.js:14803)
at updateFunctionComponent (react-dom.development.js:17034)
at beginWork (react-dom.development.js:18610)
at HTMLUnknownElement.callCallback (react-dom.development.js:188)
at Object.invokeGuardedCallbackDev (react-dom.development.js:237)
at invokeGuardedCallback (react-dom.development.js:292)
at beginWork$1 (react-dom.development.js:23203)
at performUnitOfWork (react-dom.development.js:22157)
My Sample Code
const [playerProfile, setPlayerProfile] = useState([]);
const handleSubmit = (e) => {
e.preventDefault()
}
const onChange = (e) => {
e.persist();
setPlayerProfile({ ...playerProfile, [e.target.name]: e.target.value });
}
useEffect(() => {
const fetchData = async () => {
try {
const res = await Axios.get('http://localhost:8000/service/profile')
setPlayerProfile(res.data.playerProfile);
} catch (e) {
console.log(e);
}
}
fetchData();
}, []);
return (
<div className="register_player_Twocolumn_layout_two">
<form onSubmit={handleSubmit} className="myForm">
{
playerProfile.map(({ id, image, name, email, position, privilege, password }) =>(
<div>
<div key={id} className="formInstructionsDiv formElement">
<h2 className="formTitle">Player Profile</h2>
<div className="register_profile_image">
<input id="profilePic" name="photo" type="file"/>
</div>
<div className="previewProfilePic" >
<img alt="" error="" name="previewImage" className="playerProfilePic_home_tile" src=""></img>
</div>
</div>
<div className="fillContentDiv formElement">
<label>
<input className="inputRequest formContentElement" name="name" type="text" key={name} value={name} onChange={onChange}/>
</label>
<label>
<input className="inputRequest formContentElement" name="email" type="text"/>
</label>
<label>
<div className="select" >
<select name="privilege" id="select">
<option value="player">PLAYER</option>
<option value="admin">ADMIN</option>
</select>
</div>
</label>
<label>
<input className="inputRequest formContentElement" name="password" type="password"/>
</label>
</div>
<div className="submitButtonDiv formElement">
<button type="submit" className="submitButton">Save</button>
</div>
</div>
))
}
</form>
</div>
);
TLDR: Check the Sandbox
#soccerway, based on our comments pointed out as per typos in you approach, here is some code that attempts to fix them. The link to the Live Codesandbox
SOME Context
When you define you playerProfile component state, you initialize it as an array, successfully update it from the server as an array but mess it up in the input onChange handler. Let's say you type s in the name input. With this...
setPlayerProfile({ ...playerProfile, [e.target.name]: e.target.value });
...you are transforming the playerProfile form this array.
// Fetched playerProfile from the api.
playerProfile = [
{
name: "David",
email: "david#testmail.com",
phonenumber: null,
id: 5,
privilege: "PLAYER",
photo: "C:\\fakepath\\city.JPG",
position: "FORWARD",
updatedAt: "2020-05-25T11:02:16.000Z"
},
// Extra profile put to have a solid example
{
name: "Goriath",
email: "goriath#testmail.com",
phonenumber: null,
id: 5,
privilege: "PLAYER",
photo: "C:\\fakepath\\goriath.JPG",
position: "MIDI",
updatedAt: "2020-05-26T11:02:16.000Z"
},
]
// To This Object
playerProfile = {
0: {
name: "David",
email: "david#testmail.com",
phonenumber: null,
id: 5,
privilege: "PLAYER",
photo: "C:\\fakepath\\city.JPG",
position: "FORWARD",
updatedAt: "2020-05-25T11:02:16.000Z"
},
1: {
name: "Goriath",
email: "goriath#testmail.com",
phonenumber: null,
id: 6,
privilege: "PLAYER",
photo: "C:\\fakepath\\goriath.JPG",
position: "MIDI",
updatedAt: "2020-05-26T11:02:16.000Z"
},
name: Davids"
}
Like you see, you cannot map over an object unless if you get its keys or entries, in which case the approach would still be invalid by the second element in the object.
The other issue is that you are trying to update an object, and directly appending it to the array/object. If the update is successful, this will result into duplicate data save for the name. You need to find the old object in state and update it, then totally replace it. That would be fine if your data was normalized, like saved by keys initially. Something like this...
data= {
playerProfilesById = {
5: { // Player ID is the key
name: "David",
email: "david#testmail.com",
phonenumber: null,
id: 5,
privilege: "PLAYER",
photo: "C:\\fakepath\\city.JPG",
position: "FORWARD",
updatedAt: "2020-05-25T11:02:16.000Z"
},
6: {
name: "Goriath",
email: "goriath#testmail.com",
phonenumber: null,
id: 6,
privilege: "PLAYER",
photo: "C:\\fakepath\\goriath.JPG",
position: "MIDI",
updatedAt: "2020-05-26T11:02:16.000Z"
},
},
playerProfileIds=[5,6]
}
That way its easy to update playerProfilesById with your approach, with the [e.target.id](assuming you're passing the input tag it's id) not [e.target.name], while using the playerProfileIds to map over the items in the jsx.
However if you don't have control over the api data format, you could instead make sure you update your handler to receive an id(assuming the id is unique) from the onChange, use that id to find the profile in the array.
While finding, you could keep the element array Index, and use it to directly target and update the array. (Commented out approach in handler)
Or you could just map over the entire array and update the profile that changed, then use that data to eventually update state.
Below is the full approach.
import React, { useState, useEffect } from "react";
// import axios from "axios";
/* Assuming your api returns data in the follwoing format... */
const fakeAPICall = () => {
// CALL TO AXIO MUTED
// const res = await axios.get("http://localhost:8000/service/profile");
// NOTE: Please normalize this data so it's easy to update
// READ ABOUT: https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape
const data = {
playerProfile: [
{
name: "David",
email: "david#testmail.com",
phonenumber: null,
id: 5,
privilege: "PLAYER",
photo: "C:\\fakepath\\city.JPG",
position: "FORWARD",
updatedAt: "2020-05-25T11:02:16.000Z"
},
{
name: "Goriath",
email: "goriath#testmail.com",
phonenumber: "1234345234",
id: 6,
privilege: "PLAYER",
photo: "C:\\fakepath\\goriath.JPG",
position: "MIDFIELDER",
updatedAt: "2020-05-26T11:02:16.000Z"
}
]
};
return { data };
};
const PlayerProfile = () => {
// Note that your player profile is defined as an array in state.
// Remember to always keep it that way when updating it.
const [playerProfile, setPlayerProfile] = useState([]);
const handleSubmit = e => {
e.preventDefault();
};
// Pass the id to the handler so you will know which item id changing.
const handleChange = (e, id) => {
e.persist();
let itemIndex;
const targetPlayer = playerProfile.find((player, index) => {
console.log({ player, id, index });
itemIndex = index; // Track the index so you can use it to update later.
return player.id === id;
});
console.log({ targetPlayer, id, e });
const editedTarget = {
...targetPlayer,
[e.target.name]: e.target.value
};
const tempPlayers = Array.from(playerProfile);
tempPlayers[itemIndex] = editedTarget;
/*
// Alternatively:: you can just map over the array if you dont want to track the index
const tempPlayers = playerProfile.map((profile, index) => {
return profile.id === id ? editedTarget : profile;
});
*/
setPlayerProfile(tempPlayers);
};
useEffect(() => {
const fetchData = async () => {
try {
// const res = await axios.get("http://localhost:3000/api/products");
const res = await fakeAPICall();
console.log({ response: res });
setPlayerProfile(res.data.playerProfile);
} catch (e) {
console.log(e);
}
};
fetchData();
}, []);
console.log({ "⚽: playerProfile": playerProfile });
return (
<div className="register_player_Twocolumn_layout_two">
<h1>CAPTURE PLAYER PROFILE</h1>
<p>Form to capture player Profile</p>
<hr />
<form onSubmit={handleSubmit} className="myForm">
{playerProfile.map(
({ id, image, name, email, position, privilege, password }) => (
<div key={id}>
{/*2. Also put the key on the outer div in the map above */}
<div className="formInstructionsDiv formElement">
<h2 className="formTitle">Player Profile</h2>
<div className="register_profile_image">
<input id="profilePic" name="photo" type="file" />
</div>
<div className="previewProfilePic">
<img
alt=""
error=""
name="previewImage"
className="playerProfilePic_home_tile"
src=""
/>
</div>
</div>
<div className="fillContentDiv formElement">
<label>
NAME
<input
className="inputRequest formContentElement"
name="name"
type="text"
// key={name} // Remove this key or renmae it to id. Since name changes on rerender, it confuses react that the key is different and forces the element to toose focus
value={name}
onChange={e => handleChange(e, id)} // Pass the ID form here.
/>
</label>
<label>
<input
className="inputRequest formContentElement"
name="email"
type="text"
/>
</label>
<label>
<div className="select">
<select name="privilege" id="select">
<option value="player">PLAYER</option>
<option value="admin">ADMIN</option>
</select>
</div>
</label>
<label>
<input
className="inputRequest formContentElement"
name="password"
type="password"
/>
</label>
</div>
<div className="submitButtonDiv formElement">
<button type="submit" className="submitButton">
Save
</button>
</div>
</div>
)
)}
</form>
</div>
);
};
export default function App() {
return (
<div className="App">
<PlayerProfile />
</div>
);
}
PS: When you are mapping over items, each direct wrapper expects a unique key prop, that way react can know which component exactly changed to a avoid re-renders. In your approach, you're assigning the key to the input deep in the tree. Move it up to the outer most div wrapper.
Also make sure that whatever item you use as the key is unique, otherwise the items will keep loosing focus on updates if the key changes. For example in your code, the name is being changed but you are using it as the input. This results into a new key meaning you are working on a new element, eventually loosing focus on that input.
This might be one of your problem
<label>
<input className="inputRequest formContentElement"
name="name" type="text" key={name} value={name}
onChange ={onChange}/>
</label>
The value "name" of the name property of the input tag is not in the array of playerProfile.
I think it should be:
<label>
<input className="inputRequest formContentElement"
name={name} type="text" key={name} value={name}
onChange ={onChange}/>
</label>
that means your problem should be here
setPlayerProfile({ ...playerProfile, [e.target.name]: e.target.value });
playerProfile was an array, but the above line set's it to an object that's y it's throwing the error
This might work:
setPlayerProfile([ ...playerProfile, [e.target.name]: e.target.value ]);

React form not filling out with fetched data

I am currently building a react application with a .net core back end. My current issue lies in a view that is meant to edit an article (which is made up of only a title and description). On componentDidMount, I am getting the route param id from the route and retrieving the article from the server with it (I've verified that this works correctly). My issue is that my form is not filling out with the fetched data. I'm of the understanding that since the form fields set to this.state... then they should update as the state updates however this is not what I'm seeing. I believe the issue is may lie with the warning I'm receiving in console:
index.js:2177 Warning: A component is changing a controlled input of
type hidden to be uncontrolled. Input elements should not switch from
controlled to uncontrolled (or vice versa). Decide between using a
controlled or uncontrolled input element for the lifetime of the
component.
I've read the documentation the warning points to and am not seeing how my component violates this.
My component is below in full:
import React, { Component } from 'react';
import CKEditor from 'react-ckeditor-component';
export class ArticlesEdit extends Component {
displayName = ArticlesEdit.name
constructor(props) {
super(props);
this.state = {
title: '',
description: ''
};
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount () {
const { id } = this.props.match.params;
fetch(`https://localhost:44360/api/articles/${id}`)
.then((article) => {
this.setState({
title: article.title,
description: article.description
});
});
}
updateDescription(event){
this.setState({description: event.target.value});
}
render() {
return(
<form onSubmit={this.handleSubmit} >
<div className="form-group row" >
<label className=" control-label col-md-12" htmlFor="Title">Title</label>
<div className="col-md-4">
<input className="form-control" type="text" id="title" name="title" defaultValue={this.state.title} required />
</div>
</div >
<CKEditor activeClass="editor" content={this.state.description} events= {{"change": this.onEditorChange.bind(this) }} />
<input type="hidden" id="description" name="description" value={this.state.description} onChange={this.updateDescription}/>
<div className="form-group">
<button type="submit" className="btn btn-default">Save</button>
</div >
</form >
);
}
onEditorChange(evt){
var newContent = evt.editor.getData();
this.setState({
description: newContent
});
}
handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.target);
console.log(this.state.title);
// POST request for Add employee.
fetch('https://localhost:44360/api/articles/', {
method: 'PUT',
body: data
}).then((response) => response.json())
.then((responseJson) => {
this.props.history.push("/articles");
})
}
}
You are not parsing the JSON you get as response to your fetch in componentDidMount. If you add .then((response) => response.json()) it should work as expected.
componentDidMount () {
const { id } = this.props.match.params;
fetch(`https://localhost:44360/api/articles/${id}`)
.then((response) => response.json())
.then((article) => {
this.setState({
title: article.title,
description: article.description
});
});
}
You also need to use the value prop instead of the defaultValue prop on your input so that it will have the value of title in your state.
<input
className="form-control"
type="text" id="title"
name="title"
value={this.state.title}
required
/>

Random set key is being cached?

I am following a tutorial for React/Redux (https://youtu.be/WQMglp-JASk). This part of the tutorial adds a simple form to the page and adds each entry into another section. Here is the code for adding the new info to the page:
addNinja = (ninja) => {
ninja.id = Math.random();
let ninjas = [...this.state.ninjas, ninja];
this.setState({
ninjas: ninjas
});
};
The ninja object is being passed to this method from this code:
state = {
name: null,
age: null,
belt: null
};
handleChange = (e) => {
this.setState({
[e.target.id]: e.target.value
});
};
handleSubmit = (e) => {
e.preventDefault();
this.props.addNinja(this.state);
};
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<label htmlFor="name">Name:</label>
<input type="text" id="name" onChange={this.handleChange}/>
<label htmlFor="age">Age:</label>
<input type="text" id="age" onChange={this.handleChange}/>
<label htmlFor="belt">Belt:</label>
<input type="text" id="belt" onChange={this.handleChange}/>
<button>Submit</button>
</form>
</div>
);
}
And the component JSX that is reused on submit:
<div className="ninja" key={ninja.id}>
<div>Name: {ninja.name}</div>
<div>Age: {ninja.age}</div>
<div>Belt: {ninja.belt}</div>
<button onClick={() => {deleteNinja(ninja.id)}}>x</button>
</div>
ninja.id is the key for the element being added.
The first submit works fine, any subsequent submits use the same key generated by Math.random() on the first submit. I also see the warning Encountered two children with the same key in the console for each subsequent submit.
When I console log the random number it is a different each time, but ninja.id is not being set to the new random number.
I assume caching is at play here but can someone please explain why this might be happening?
Well, the way you are adding the id for your ninja you are mutating your state which is a big no-no for react. So the following:
let ninjas = [...this.state.ninjas, ninja];
should be:
let ninjas = [...this.state.ninjas,
{
id: Math.random(),
name: this.state.name,
age: this.state.age,
belt: this.state.belt
}
];
There are definitely better ways for using as id for iterating items within an array such as https://www.npmjs.com/package/uuid
Ok, so I actually got this working myself but I would love for someone to tell me what the issue is.
To get it working, the AddNinja method should be:
addNinja = (ninja) => {
let ninjas = [...this.state.ninjas, {
"name": ninja.name,
"age": ninja.age,
"belt": ninja.belt,
"id": Math.random()
}];
this.setState({
ninjas: ninjas
});
};
I have no idea why this works and the other version doesn't so any help would be appreciated :)

How to save previous state/ page data before going to another page in React JS

this.state = {
name: '',
color: '',
description: '',
beacons: []
};
handleColors = (item) => {
this.setState({
color: item.value
});
};
handleBeaconChange = (beacons) => {
this.setState({
beacons
});
};
handleInputChange(event) {
this.setState({
[event.target.name]: event.target.value
});
}
<div className="mb-1 margin-input">
<span className=" form-font div-width">NAME<span className="font-css top">*</span></span>
<input type="text"
className="form-control margin-select"
placeholder="Name"
value={this.state.name}
name="name"
onChange={this.handleInputChange}
/>
</div>
<div className="mb-1">
<span className=" form-font div-width">
COLOR</span>
<Select
className="margin-select"
value={this.state.color}
options={colorValues}
onChange={this.handleColors}/>
</div>
<div className="mb-1 margin-input">
<span className=" form-font div-width">DESCRIPTION</span>
<textarea
className="form-control margin-select"
placeholder="Description"
value={this.state.description}
name="description"
onChange={this.handleInputChange}
/>
</div>
<h3 className="">{this.props.label}</h3>
<div className="mb-1">
<span className=" form-font div-width">
BEACON (s)</span>
<Select.Async
multi={true}
className="margin-select"
value={this.state.beacons}
loadOptions={getBeacons}
onChange={this.handleBeaconChange}/>
<div className="panel-body">
Add Beacon
</div>
</div>
This is the image.
Now i just wanted to save previous form data before going to another page
Like for example in this picture i had entered name and color values in the fields and when i click on "Add beacon link", it will route me to another form but the values i entered in this form were lost when i will come back to it.
Any solutions regarding that?
You can make use of Redux to achieve the same. What you need to do is to save the form states in redux store rather than the component localState itself
You reducer will look something like
const initialState = {
name: '',
color: '',
description: '',
beacons: []
}
const BeaconForm = (state = initialState, action) => {
switch(action.type) {
case 'UPDATE_BEACON_FORM_VALUE':
return {
...state, [action.newVal.key]: action.newVal.value
}
default: return state
}
}
export default BeaconForm
And then have an action
export function updataFormValue(updatedVal) {
return { type: 'UPDATE_BEACON_FORM_VALUE', newVal: updatedVal}
}
And in the compoennt
handleInputChange(event) {
var data = {[event.target.name]: event.target.value}
this.props.updataFormValue(data);
}
Apart from this you need to make your component Redux compatible with connect, mapStateToProps , etc which I assume you already know
And then instead of setting input value from state you will set it from props that youu get from redux store

Resources