I have a form component in my react app that can be used to create or update recipes depending on the route. The create functionality works fine, but the update is giving me trouble.
I pass fetched data into the component using props here is what the JSON looks like:
{
method: "Cook the guanciale in a large skillet over medium heat until deeply golden
(adjust the heat as necessary to render the fat [...]
name: "Pasta Alla Gricia"
}
I am trying to get the name to prefill into the form's name <input> and the method to prefill into the form's method <textarea>. I tried doing this with useEffect(), with:
useEffect(() => {
setName(props.data.name)
setMethodStepsList(props.data.method)
})
while it prefilled the name input it then locked the value to that. The method did not prefill the textarea at all.
I am pretty stumped with this one, and would be grateful for any assistance.
export default function Recipe(props) {
const [name, setName] = useState('')
const [methodStepsList, setMethodStepsList] = useState('')
const [methodStepObject, setMethodStepObject] = useState([])
const [ingredientList, setIngredientList] = useState([])
const [ingredientObject, setIngredientObject] = useState({
ingredient_name: '',
quantity: '',
measure: '',
})
const formLabel = props.data ? 'Update Recipe' : 'New Recipe'
useEffect(() => {
setName(props.data.name)
setMethodStepsList(props.data.method)
})
//new recipe logic
[...]
return (
<div>
<div className="recipe-form-container">
<form className="recipe-form">
<div className="page-header">
<h1>{formLabel}</h1>
</div>
{/* recipe name logic */}
<div className="recipe-title recipe-element">
<label>Recipe Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
></input>
</div>
//recipe method logic
<div className="recipe-blurb recipe-element">
<label>Recipe Method</label>
<span className="method-span">
<textarea
rows="5"
name="step_instructions"
type="text"
placeholder="Method will be split up based on new lines"
onChange={(e) => handleMethodChange(e)}
></textarea>
<button
onClick={(e) => {
console.log(methodStepObject)
setMethodStepsList(methodStepObject)
e.preventDefault()
}}
>
Add Method
</button>
[...]
}
Please remove useEffect statements and try like this
const [name, setName] = useState(props.data.name)
const [methodStepsList, setMethodStepsList] = useState(props.data.method)
You should be careful while initializing state with props in React.
See React component initialize state from props
class Recipe extends React.Component {
constructor(props) {
super(props)
this.state = {name:'', method:'', ingredients: []};
}
static getDerivedStateFromProps(props,state) {
return {name: props.data.name, method: props.data.method};
}
render() {
return <div>...</div>
}
}
Related
I'm trying to create a reusable "Modal" component in React. The Modal component will have some input fields and a submit button and when user clicks the Submit button in the Modal. the modal should be closed and the data user entered in the input fields should be passed up to the parent component through a callback function declared in parent component. The code is working fine but i think(not sure) it's not the right solution as i had to create "React state" in both the Modal(child) component and the parent component where the Modal component is used. As you can see(sandbox code) the state is repetitive in both components and i was wondering how can i keep the state in only one place. My understanding is I'd definitely need to create a state in Modal component for the input fields to keep track of changes but i am not sure how can i use that data in parent component without creating same state
Here is a Sandbox that i created to show what i've done so far:
https://codesandbox.io/s/jovial-matsumoto-1zl9b?file=/src/Modal.js
In order to not duplicate state I would enclose the inputs in the modal in a form element and convert the inputs to uncontrolled inputs. When the form is submitted grab the form field values and pass in the formData callback and reset the form. Explicitly declare the button to be type="submit".
const Modal = (props) => {
const onFormSubmit = (e) => {
e.preventDefault();
const firstName = e.target.firstName.value;
const lastName = e.target.lastName.value;
props.formData({ firstName, lastName });
e.target.reset();
};
if (!props.show) {
return null;
} else {
return (
<div className="modal" id="modal">
<form onSubmit={onFormSubmit}>
<input type="text" name="firstName" />
<input type="text" name="lastName" />
<button className="toggle-button" type="submit">
Submit
</button>
</form>
</div>
);
}
};
It seems the crux of your question is about the code duplication between your parent component and a modal. What I would really suggest here is to decouple the modal from any specific use case and allow any consuming parent components to pass a close handler and children components.
This keeps the state in the parent, and thus, control.
Example modal component:
const Modal = ({ onClose, show, children }) =>
show ? (
<div className="modal" id="modal">
<button type="button" onClick={onClose}>
X
</button>
{children}
</div>
) : null;
Parent:
function App() {
const [isShowModal, setIsShowModal] = useState(false);
const [firstName, setFirstName] = useState();
const [lastName, setLastName] = useState();
const showModal = (e) => {
setIsShowModal((show) => !show);
};
const closeModal = () => setIsShowModal(false);
const onFormSubmit = (e) => {
e.preventDefault();
const firstName = e.target.firstName.value;
const lastName = e.target.lastName.value;
setFirstName(firstName);
setLastName(lastName);
e.target.reset();
closeModal();
};
return (
<div className="App">
<h2>First Name is : {firstName}</h2>
<h2>Last Name is : {lastName}</h2>
<button className="toggle-button" onClick={showModal}>
Show Modal
</button>
<Modal show={isShowModal} onClose={closeModal}>
<form onSubmit={onFormSubmit}>
<input type="text" name="firstName" />
<input type="text" name="lastName" />
<button className="toggle-button" type="submit">
Submit
</button>
</form>
</Modal>
</div>
);
}
Here it's really up to you how you want to manage the interaction between parent and modal content. I've shown using a form and form actions so your state isn't updated until a user submits the form. You can use form utilities (redux-form, formix, etc...) or roll your own management. Life's a garden, dig it.
You can define these states in your App component :
const [showModal, setShowModal] = useState(false);
const [user, setUser] = useReducer(reducer, {
firstName: "",
lastName: ""
});
const toggleModal = () => {
setShowModal(!showModal);
};
And pass them through props when you render your Modal component :
<div className="App">
<h2>First Name is : {firstName}</h2>
<h2>Last Name is : {lastName}</h2>
<button className="toggle-button" onClick={toggleModal}>
Show Modal
</button>
<Modal
show={showModal}
hide={() => toggleModal(false)}
user={user}
updateUser={setUser}
/>
</div>
Then, in your Modal component, define states for firstName and lastName :
const Modal = (props) => {
const [firstName, setFirstName] = useState(props.user.firstName);
const [lastName, setLastName] = useState(props.user.lastName);
const onFirstNameChange = ({ target }) => {
setFirstName(target.value);
};
const onLastNameChange = ({ target }) => {
setLastName(target.value);
};
// ...
}
And now you can submit changes like this from your Modal component :
const onFormSubmit = (e) => {
props.updateUser({
firstName,
lastName
});
props.hide();
};
I would like to understand why is it when I'm switching between posts, the input fields are not changing their values, even though each product object has different name and description property.
Further explanation:
When clicking on each ProductView (Product Component) a new window is shown with details on that product that could be changed and saved (name and description) through input fields. but when switching between products (by clicking on different products) the text on these input fields do not change.
example product object:
product = {
name: 'product 1',
desc: 'product 1 desc'
}
this is the code:
// Main Store Component
const Store = () =>{
const [prods, setProducts] = useState([...products]);
const[show, showDetails] = useState(false);
const[productToShow, setProductToShow]=useState();
function onSaveProduct(newProduct){
let i = prods.findIndex((x)=> x['id']===newProduct.id);
prods[i] = newProduct;
setProductToShow(newProduct)
setProducts([...prods]);
}
return(<div className={'flex-container'}>
<Container className="store-container">
<div className={'column-align'}>
{([...prods]).map((pro)=>{
return <Product key={pro.id} product={pro} showDetails={showDetails}
setProductToShow={setProductToShow}/>
})}
</div>
</Container>
{show && <ProductToShow product={productToShow} onSave={onSaveProduct}/>}
</div>);
}
// Product component
const Product = ({product, setProductToShow, showDetails, deleteFromList}) =>{
const handleClick = () =>{
setProductToShow(product);
showDetails(true);
}
return (
<div className="product-container">
<div className="name-desc"onClick={handleClick}>
<h3>{product.name} </h3>
<h5>{product.desc}</h5>
</div>
</div>
);
}
// ProductToShow functional component
const ProductToShow = ({product, onSave}) =>{
const nameInput = useFormInput(product.name);
const descInput = useFormInput(product.desc);
const newProduct = {
id: product.id,
name: nameInput.value,
desc: descInput.value,
};
function useFormInput(initialValue){
const[value, setValue] = useState(initialValue);
function handleChangeEvent(e){
setValue(e.target.value);
}
return{
value: value,
onChange: handleChangeEvent
}
}
return (
<div className="to-show-container">
<h1>{product.name}</h1>
<label>Product Name: </label>
<input {...nameInput}/>
<label>Product Description: </label>
<input {...descInput}/>
<div className={'to-the-right'}>
<Button onClick={()=>onSave(newProduct)}>Save</Button>
</div>
</div>
);
}
screenshot (Product 3 is clicked, but the details of Product 1 is shown in the input fields):
The problem is in your productToShow functional component.
The values don't get reupdated after clicking on a different product.
Maybe consider changing it to something like this:
// ProductToShow functional component
const ProductToShow = ({ product, onSave }) => {
const [name, setName] = useState(product.name);
const [desc, setDesc] = useState(product.desc);
useEffect(() => {
setName(product.name);
setDesc(product.desc);
}, [product]);
return (
<div className="to-show-container">
<h1>{product.name}</h1>
<label>Product Name: </label>
<input value={name} onChange={(e) => setName(e.target.value)} />
<label>Product Description: </label>
<input value={desc} onChange={(e) => setDesc(e.target.value)} />
<div className={"to-the-right"}>
<Button
onClick={() => onSave({id:product.id,name,desc})}>
Save
</Button>
</div>
</div>
);
};
I used useState since I don't know if you want to use it further in that component or not.
If the only purpose of the component is to update the products, I would take the advice of the other answer and wrap the inputs in a form tag and take the inputs after submitting the form
It could be problem with reinitialization of inputs.
ProductToShow component is the same for any selected product. You pass props (they could be changed) but I'm not sure if nameInput is changed here after product props changes:
const nameInput = useFormInput(product.name);
I think it's better to wrap inputs with the <form>...</form> and control reinitialization with initialValues.
also:
would be helpful to have codesandbox or something.
not need to use [...spread] if spead is array[].
not need to have to states for show / not-to-show & productToShow. Just use productToShow with null option when you don't want to show any product.
Below is my code for a personal project where i can keep track of my monthly subscriptions, if i have to add a subscription i just have a add an object to an existing array. however for testing purposes when i tried to console.log(value.startDate) in handleSubmit it gives me undefined and causes further problems. How would i fix it?
import React from 'react';
import PropTypes from 'prop-types';
const List = () => {
const [ mylist, setList ] = React.useState([]);
const [ value, setValue ] = React.useState({ subscription: '', startDate: '', paymentTime: 0 });
const handleSubmit = (e) => {
console.log(value.startDate);
setList(mylist.push(value));
e.preventDefault();
};
const handleOnChange = (event) => {
setValue({ [event.target.name]: event.target.value });
};
return (
<div>
<div className="for_list">
<ul className="list">{mylist.map((obj) => <li key={obj.subscription}>{obj.subscription}</li>)}</ul>
</div>
<div className="for_form">
<form>
<input type="text" name="subscription" onChange={handleOnChange} value={value.subscription} />
<input type="text" name="startDate" onChange={handleOnChange} value={value.startDate} />
<input type="number" name="paymentTime" onChange={handleOnChange} value={value.paymentTime} />
</form>
</div>
<button onClick={handleSubmit}>Add Item</button>
</div>
);
};
// it just removes the error above.
List.propTypes = {
list: PropTypes.node
};
export default List;
You are replacing your state every time. This might be because of the miss in understanding the difference between setState in traditional class based React components and useState.
You need to append the value to the existing data. Something similar would work
const handleOnChange = (event) => {
setValue({ ...value, [event.target.name]: event.target.value });
};
The setState in class based components always accepts partial state and merges with the existing one. While useState setter function replaces the value you provide in the respective state.
On handleChange function you need to pass the old value of value
const handleOnChange = (event) => {
setValue({ ...value , [event.target.name]: event.target.value });
};
I'm currently using this plugin for my react application: https://www.npmjs.com/package/react-editext.
I have multiple fields:
<EdiText
value={contact.addressLine1}
type="text"
onSave={handleSave('addressLine1')}
onCancel={(e) => setEditing(v => !v)}
inputProps={{
placeholder: 'Address Line 1',
}}
/>
<EdiText
value={contact.addressLine2}
type="text"
onSave={handleSave('addressLine2')}
onCancel={(e) => setEditing(v => !v)}
inputProps={{
placeholder: 'Address Line 2',
}}
/>
With a save handle
const handleSave = (e) => value => {
setContact({...contact, [e]: value})
};
But, I need to be able to save all fields with one button.
Now, if these were controlled form fields, I would be able to grab the value, and submit. But they're not as there is no onChange event.
Any ideas?
I didn't find in the plugin a possibility to do that. I suggest that you use a form with refs to achieve what you want.
here is an example code
import React, { useState, useRef } from "react";
import "./styles.css";
export default function App() {
const [editing, setEditing] = useState(true);
const [contact, setContact] = useState({
addressLine1: "Address 1",
addressLine2: "Address 2"
});
const [adress1, setAdress1] = useState("adress 1");
const [adress2, setAdress2] = useState("adress 2");
const form = useRef(null);
const handleSave = () => {
const adresses = {
addressLine1: form.current["adress1"].value.toString(),
addressLine2: form.current["adress2"].value.toString()
};
setContact(adresses);
console.log(contact);
};
const handleEdit = () => {
const edit = editing;
setEditing(!edit);
};
return (
<div className="App">
<form ref={form}>
<input
type="text"
value={adress1}
name="adress1"
onChange={e => setAdress1(e.target.value)}
disabled={editing}
/>
<input
type="text"
value={adress2}
name="adress2"
onChange={e => setAdress2(e.target.value)}
disabled={editing}
/>
</form>
<button onClick={handleSave}>save</button>
<button onClick={handleEdit}>edit</button>
</div>
);
}
explanation
I used state variable editing to make the fields editable or not on button edit click
I used a state variable for each field and used the react onChange function to save the value of each field when it changes.
on save button click the values of all fields states get saved to contact state
you can change the code to make it suitable for your needs. Here is a sandbox for my code:https://codesandbox.io/s/eloquent-pine-wnsw3
class App extends Component {
constructor() {
super()
this.state = {
firstName: ""
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(event) {
this.setState({
firstName: event.target.value
})
}
render() {
return (
<form>
<input type="text" placeholder="First Name" onChange={this.handleChange} />
<h1>{this.state.firstName}</h1>
</form>
);
}
}
export default App;
Hello all, I am currently studying React and seem to be having a hard time grasping all of it. The code that I have here works in that it will show in browser what the user is typing in the input box. What I cannot seem to figure out or get to work, is mapping what is typed in the input to stay on the screen. I.e. when I hit enter, it refreshes and the name goes away. I am trying to now create an unordered list to keep each name displayed on the screen. Any help or links would be greatly appreciated. Thank you
Just add new function (this describe what should be after submit this form) in this case You use:
event.preventDefault() -
The Event interface's preventDefault() method tells the user agent
that if the event does not get explicitly handled, its default action
should not be taken as it normally would be
onSubmit(event){
event.preventDefault()
}
and on form:
<form onSubmit={this.onSubmit}>
To create unordered list use something like this (credit for Robin Wieruch):
import React from 'react';
const initialList = [
'Learn React',
'Learn Firebase',
'Learn GraphQL',
];
const ListWithAddItem = () => {
const [value, setValue] = React.useState('');
const [list, setList] = React.useState(initialList);
const handleChange = event => {
setValue(event.target.value);
};
const handleSubmit = event => {
if (value) {
setList(list.concat(value));
}
setValue('');
event.preventDefault();
};
return (
<div>
<ul>
{list.map(item => (
<li key={item}>{item}</li>
))}
</ul>
<form onSubmit={handleSubmit}>
<input type="text" value={value} onChange={handleChange} />
<button type="submit">Add Item</button>
</form>
</div>
);
};
export default ListWithAddItem;