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();
};
Related
I am developing an chrome extension where i need to authentication user but a very simple onClick button which calls a function is not working
this is the simple code where i want to show info on console when button is clicked
import React, { useState } from 'react';
const Login = () => {
const [user, setuser] = useState("");
const handleSubmit = (data) => {
data.preventDefault();
console.log("usernae: ");
console.log("Data: ", data.target);
}
const getInputValue = (event) => {
console.log(event.target.value)
// Select input element and get its value
console.log("I am heresdfg")
// let inputVal = document.getElementsByClassName("usernameInputField")[0].value;
// Display the value
// alert(inputVal);
}
return (
<div
id="login-form">
<p>
<div className='form'>
</div>
<input type="text"
id="username"
name="username"
className='usernameInputField'
value={user}
onChange={(event => setuser(event.target.value))}
placeholder="Username" required />
</p>
<p>
<button onClick={getInputValue} type="button" id="login">button</button>
</p>
</div>
);
};
export default Login;
It seems like you want the input value value inside the event handler if I'm not wrong, you can get it from the state - user as
const getInputValue = (event) => {
console.log(user)
}
as the event would be button's you wouldn't get the value of input from it's event and it is not required too as it's already in the react's state ....
Example:
const {useState} = React;
const App = () => {
const [name, setName] = useState("");
const submitHandler = () => {
console.log(name)
}
return (
<div>
Name: <input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>
<button onClick={submitHandler}>Submit</button>
</div>
);
};
ReactDOM.createRoot(
document.getElementById("root")
).render(
<App/>
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
In the getInputValue function event is pointing to the button.
Change the event.target.value to user if you want to print the text into the console.
Here's the codesandbox.
If you don't want to use the value from useState then you can also check useRef hook which works in a similar way.
I have a page /studentprofile where user can look at their profile details, when the user clicks the button 'edit profile' they are brought to /editprofile and is able to update their profile with a form. Once they click on the button 'update', editProfile() function is called and the api updates the details respectively then navigates the user back to /studentprofile. However, once the user goes back to /studentprofile their old data is only shown and you have to manually reload the page to see the updated data, is there any way to fix this so the user don't have to reload the page themselves?
Here is the code of /editprofile. I am using react router dom v6.3.0
function StudentEdit() {
const navigate = useNavigate();
const [name, setName] = useState("")
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [description, setDescription] = useState("")
const data = {
student_name: name,
email: email,
password: password,
description: description
}
function editProfile() {
const studentid = localStorage.getItem('studentid')
api.put(`/students/${studentid}`, data)
.then(
navigate('/studentProfile')
)
}
return (
<div className={editstyle.box}>
<form className={editstyle.form}>
<h2>Edit Profile</h2>
<input type='text' className={editstyle.input} placeholder='Name' onChange = {(e) => setName(e.target.value)} id ="postName" value={name}></input>
<input type='text' className={editstyle.input} placeholder='Email' onChange = {(e) => setEmail(e.target.value)} id ="postEmail" value={email}></input>
<input type='password' className={editstyle.input} placeholder='Password' onChange = {(e) => setPassword(e.target.value)} id ="postPassword" value={password}></input>
<input type='text' className={editstyle.input} placeholder='Description' onChange = {(e) => setDescription(e.target.value)} id ="postDescription" value={description}></input>
<div className={editstyle.btncol}>
<button type="update" className={editstyle.btn} onClick={() => editProfile()}>Update</button>
<button type="reset" className={editstyle.btn}>Discard Changes</button>
</div>
</form>
</div>
)
}
export default StudentEdit;
here is the component of /studentprofile
function StudentProfile() {
const [profile, setProfile] = useState([])
const navigate = useNavigate();
useEffect(() => {
const fetchProfile = async () => {
try {
const studentid = localStorage.getItem('studentid')
const response = await api.get(`/students/${studentid}`);
setProfile(response.data);
} catch (err) {
if (err.response) {
console.log(err.response.data);
} else {
console.log(`error: ${err.message}`)
}
}
}
fetchProfile()
}, [])
const delProfile = async () => {
try {
const studentid = localStorage.getItem('studentid')
await api.delete(`/students/${studentid}`);
navigate('/home');
} catch (err) {
console.log(`Error: ${err.message}`);
}
}
return(
<div>
{profile.map(profiles => (
<Profile key={profiles.student_id} profiles={profiles} delProfile={delProfile}/>
))}
</div>
)
}
export default StudentProfile;
app.js routing
function App() {
return (
<div>
<NavBar/>
<BrowserRouter>
<Routes>
<Route path="/studentprofile" element={<StudentProfile/>}/>
<Route path="/studentEdit" element={<StudentEdit/>}/>
</Routes>
</BrowserRouter>
</div>
)
}
I think I see the issue now, the form element in the StudentEdit component isn't having the default form action prevented which is reloading the page and React app.
The "update" button has an invalid type attribute value. The only valid type attribute values are "submit" (which is the default value), "button", and "reset". I suspect the form is being submitted and reloading the React app.
Fix the update button's type attribute and prevent the form element's default form action.
Another issue is the api.put Promise chain, the .then has an invalid callback, it's immediately invoking the navigate function. This should be a proper callback function.
Example:
function StudentEdit() {
...
function editProfile(e) { // <-- receive `onSubmit` event object
e.preventDefault(); // <-- prevent the default action
const studentid = localStorage.getItem("studentid");
api.put(`/students/${studentid}`, data)
.then(() => navigate("/studentProfile")); // <-- callback calls `navigate`
}
return (
<div className={editstyle.box}>
<form
className={editstyle.form}
onSubmit={editProfile} // <-- editProfile is onSubmit handler
>
...
<div className={editstyle.btncol}>
<button
type="submit" // <-- type="submit" here to submit form
className={editstyle.btn}
>
Update
</button>
<button type="reset" className={editstyle.btn}>
Discard Changes
</button>
</div>
</form>
</div>
);
}
Try mobx or zustand it may suit your need better
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.
This is my sample code, it's very basic i just want to console fname once submit is clicked.
When i pressed first time i get an empty line but when pressed second time the button i get some empty value. I am attaching the screenshot for the console.
I dont want to change my code to a class and use some method to get the value in console.
screenshot for console output
import React,{useState} from 'react'
export const anyComponent = () => {
const [fname, setFname] = useState('')
const submit = (event) =>{
setFname({fname: [event.target.value] })
console.log(fname)
}
return(
<div>
<input name="fname" type="text" placeholder="Enter your First Name" />
<button onClick={submit}>Submit</button>
</div>
)
}
From MDN Docs:
The target property of the Event interface is a reference to the object onto which the event was dispatched.
In your case, event.target would point to the button and not input.
What you need is a reference to the input, you can use useRef hook for it like this
import React, { useState, useRef } from "react";
export default anyComponent = () => {
const inputEl = useRef(null);
const [fname, setFname] = useState("");
const submit = event => {
setFname({ fname: [inputEl.current.value] });
};
console.log(fname);
return (
<div>
<input
name="fname"
ref={inputEl}
type="text"
placeholder="Enter your First Name"
/>
<button onClick={submit}>Submit</button>
</div>
);
};
Also, setState is asynchronous, that's why you wouldn't see the result in the console just after calling setFname. However you'd see the updated fName in console on the next render, that's why I've moved it out of the submit.
Without useRef
Alternatively, you can add onChange handler on input and update the state fname from there
function App(){
const [fname, setFname] = useState("");
const submit = () => {
console.log(fname);
};
const handleChange = ev => {
setFname({ fname: [ev.target.value] });
}
return (
<div>
<input
name="fname"
onChange={handleChange}
type="text"
placeholder="Enter your First Name"
/>
<button onClick={submit}>Submit</button>
</div>
);
};
Your console log should be outside submit method. Or log event.target.value instead fname.
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