React Controlled Form with Child /Parent component - javascript

I'm building a controlled form with dynamic fields.
The Parent component get data from a redux store and then set state with the values.
I don't want to make it with too much code lines so I turn the dynamic fields into a component.
States stay in the parent component and I use props to pass the handlechange function.
Parent :
function EditAbout(props) {
const [img, setImg] = useState("");
const [body, setBody] = useState(props.about.body);
const [instagram, setInstagram] = useState(props.about.links.instagram);
const [linkedin, setLinkedIn] = useState(props.about.links.linkedin);
const [press, setPress] = useState(props.about.press)
const handleSubmit = (e) => {
// Submit the change to redux
};
// set states with redux store
useEffect(() => {
setBody(props.about.body);
setInstagram(props.about.links.instagram);
setLinkedIn(props.about.links.linkedin);
setPress(props.about.press);
}, []);
const handleChangeChild = (e, index) => {
e.preventDefault();
let articles = press
const {value, name } = e.target
if (name === "title") {
articles[index].title = value;
} else {
articles[index].link = value;
}
setPress(articles)
console.log(articles[index])
}
return (
<Box>
<h1>CHANGE ABOUT ME</h1>
<Input
label="Image"
name="img"
type="file"
variant="outlined"
margin="normal"
onChange={(e) => setImg(e.target.files)}
/>
<Input
label="body"
value={body}
name="body"
onChange={(e) => setBody(e.target.value)}
variant="outlined"
multiline
rowsMax={12}
margin="normal"
/>
<Input
label="instagram"
value={instagram}
name="instagram"
variant="outlined"
margin="normal"
onChange={(e) => setInstagram(e.target.value)}
/>
<Input
label="Linkedin"
value={linkedin}
name="linkedin"
variant="outlined"
margin="normal"
onChange={(e) => setLinkedIn(e.target.value)}
/>
<Child press={press} onChange={handleChangeChild} />
{props.loading ? (
<CircularProgress color="black" />
) : (
<Button onClick={handleSubmit} variant="contained">
Send
</Button>
)}
</Box>
);
}
Child :
function Child(props) {
const { press, onChange } = props;
const inputsMarkup = () =>
press.map((article, index) => (
<div key={`press${index}`} style={{ display: "flex" }}>
<input
name="title"
value={press[index].title}
onChange={(e) => onChange(e, index)}
/>
<input
name="link"
value={press[index].link}
onChange={(e) => onChange(e, index)}
/>
<button>Delete</button>
</div>
));
return (
<div>
<h1>Press :</h1>
{inputsMarkup()}
</div>
);
}
Everything is fine when I'm typing in the Parent inputs. But when I'm using Child fields state update for one character but come back at its previous state right after.
It also doesn't display the character change. I can only see it in the console.
Thanks you in advance for your help

The problem is that you're mutating the state directly. When you create the articles variable (let articles = press) you don't actually create a copy and articles doesn't actually contain the value. It's only a reference to that value, which points to the object’s location in memory.
So when you update articles[index].title in your handleChangeChild function, you're actually changing the press state too. You might think that's fine, but without calling setPress() React will not be aware of the change. So, although the state value is changed, you won't see it because React won't re-render it.
You need to create a copy of the press array using .map() and create a copy of the updated array element. You can find the updated handleChangeChild() below:
const handleChangeChild = (e, index) => {
e.preventDefault();
const { value, name } = e.target;
setPress(
// .map() returns a new array
press.map((item, i) => {
// if the current item is not the one we need to update, just return it
if (i !== index) {
return item;
}
// create a new object by copying the item
const updatedItem = {
...item,
};
// we can safely update the properties now it won't affect the state
if (name === 'title') {
updatedItem.title = value;
} else {
updatedItem.link = value;
}
return updatedItem;
}),
);
};

Related

Why isn't my child component updating data when changing the state in React?

I have a list of users and I want to display in another component on the same page the user data in input fields for every user that I click on.
When no user is selected, I want the component to just render some text and a button to add a user. When the button is clicked the component renders the form with empty input fields so that we can add a new user.
I tried the following, but it's just showing the data for the first one I click on. It's not updating.
The main page:
const index = (props) => {
const [selectedUser, setSelectedUser] = useState(null);
const [state, setState] = useState("Index");
const onChange = (item) => {
setState("Edit");
setSelectedUser(item);
};
const onClick = (e, item) => {
if (e.type === "click" && e.clientX !== 0 && e.clientY !== 0) {
onChange(item);
} else {
console.log('prevented "onClick" on keypress');
}
};
const renderComponent = () => {
switch (state) {
case "Index":
return (
<>
<div className="btn" onClick={(e) => setState("Edit")}>
+ New Staff
</div>
<img src="/storage/illustrations/collaboration.svg" />
</>
);
case "Edit":
return (
<div>
<StaffForm profile={selectedUser} />
</div>
);
}
};
return (
<>
<div>
<div>
<h1>Staff</h1>
</div>
<div>
<div>
{profiles.map((item, index) => {
return (
<div key={index} onClick={(e) => onClick(e, item)}>
<input
type={"radio"}
name={"staff"}
checked={state === item}
onChange={(e) => onChange(item)}
/>
<span>{item.user.name}</span>
</div>
);
})}
</div>
<div>{renderComponent()}</div>
</div>
</div>
</>
);
};
The Staff Form Component:
const StaffForm = ({ profile }) => {
const { data, setData, post, processing, errors, reset } = useForm({
email: profile ? profile.user.email : "",
name: profile ? profile.user.name : "",
phone_number: profile ? profile.user.phone_number : "",
avatar: profile ? profile.user.avatar : "",
});
const [file, setFile] = useState(data.avatar);
const handleImageUpload = (e) => {
setFile(URL.createObjectURL(e.target.files[0]));
setData("avatar", e.target.files[0]);
};
const onHandleChange = (event) => {
setData(
event.target.name,
event.target.type === "checkbox"
? event.target.checked
: event.target.value
);
};
return (
<div>
<ImageUpload
name={data.name}
file={file}
handleImageUpload={handleImageUpload}
/>
<TextInput
type="text"
name="name"
value={data.name}
autoComplete="name"
isFocused={true}
onChange={onHandleChange}
placeholder={t("Name")}
required
/>
<TextInput
type="text"
name="phone_number"
value={data.phone_number}
autoComplete="phone_number"
placeholder={t("Phone Number")}
onChange={onHandleChange}
required
/>
<TextInput
type="email"
name="email"
value={data.email}
autoComplete="email"
onChange={onHandleChange}
placeholder={t("Email")}
required
/>
</div>
);
};
First of all something you should avoid is the renderComponent() call.Check here the first mistake mentioned in this video. This will most likely fix your problem but even if it doesn't the video explains why it should not be used.
Something else that caught my eye(possibly unrelated to your question but good to know) is the onChange function. When two pieces of state change together it is a potential source of problems, check out this article on when to use the useReducer hook.
Also be careful with naming React Components, they need to be capital case, this question contains appropriate answers explaining it.
(To only solve your problem stick to no.1 of this list, there are some improvements i'd do here overall for code quality and beauty, msg me for more details)

Conditional rendering an input inside a map

I have a list of task which inside have another task(subTask).
I use a map to list all the tasks and have a button to add more subtasks to that task.
The input button is supposed to be invisible unless the button is pressed.
The problem is that I used the useState hook to conditional render the input but whenever I click in any of the button, the inputs of all tasks open and is supposed to only open the input from that certain index.
const Task = () => {
const [openTask, setOpenTask] = useState(false);
return task.map((item, index) => {
return (
<div>
<button onClick={() => setOpenTask(!openTask)}>Add</button>
{item.name}
{openTask && <input placeholder="Add more tasks..."/>}
{item.subTask.map((item, index) => {
return (
<>
<span>{item.name}</span>
</>
);
})}
</div>
);
});
};
Try using array
const Task = () => {
const [openTask, setOpenTask] = useState([]);
const openTaskHandler = (id) => {
setOpenTask([id])
}
return task.map((item, index) => {
return (
<div>
<button onClick={() => openTaskHandler(item.id)}>Add</button>
{item.name}
{openTask.includes(item.id) && <input placeholder="Add more tasks..."/>}
{item.subTask.map((item, index) => {
return (
<>
<span>{item.name}</span>
</>
);
})}
</div>
);
});
};
That's because you are setting openTask to true for all the items. You can create object and store the specific id to true.
for eg.
const [openTask, setOpenTask] = useState({});
and then on button onClick you can set the specific id to open
setOpenTask({...openTask,[`${item.name}-${index}`]:true});
and then check against specific id
{openTask[`${item.name}-${index}`] && <input placeholder="Add more tasks..."/>}

Handling an Unknown Number of Inputs in React

I am creating a react element which allows users to send emails to friends/family. The element defaults to 3 input fields however, the user can click a button to add additional input fields.
My idea to build this component is to have a button which increments a counter and then to use that counter value to run a loop and add items to an array
<button onClick={handleAddEmail}> Add email </button>
function handleAddEmail() {
setNumberOfEmails(numberOfEmails + 1);
}
Now for this to work I need an NumberOfEmails hook. This hook will be initialized with the first three input fields.
const [numberOfEmails, setNumberOfEmails] = useState(3);
Additionally there is a inviteEmails hook to handle the state of each input.
const [inviteEmails, setInviteEmails] = useState([])
All of these inputs should be rendered in a function:
function renderEmailFields() {
const emailContainer = [
<input
value={inviteEmails[0]}
onChange={(e) => handleEmailChange(0, e)}
placeholder="Enter email here..."
/>,
<input
value={inviteEmails[1]}
onChange={(e) => handleEmailChange(1, e)}
placeholder="Enter email here..."
/>,
<input
value={inviteEmails[2]}
onChange={(e) => handleEmailChange(2, e)}
placeholder="Enter email here..."
/>];
for (let i = 3; i < numberOfEmails; i++) {
emailContainer.push(
<input
value={inviteEmails[i]}
onChange={(e) => handleEmailChange(i, e)}
placeholder="New email..."
/>
);
}
return emailContainer;
The code I have above is close to working however adding a new input field causes the previous inputs to lock and become uneditable.
Any advice would be appreciated.
You don't really need a second state atom for the number of inputs if you ask me.
See live CodeSandbox here.
The useCallback and dataset magic make it so you don't need a separate onClick handler function for each input.
.filter(Boolean) removes all falsy values; empty strings are falsy.
For the heck of it, this also lets you remove items from the array.
function App() {
const [inviteEmails, setInviteEmails] = React.useState(["", "", ""]);
const handleEmailChange = React.useCallback((event) => {
const index = parseInt(event.target.dataset.index, 10);
setInviteEmails((inviteEmails) => {
const newInviteEmails = [...inviteEmails];
newInviteEmails[index] = event.target.value;
return newInviteEmails;
});
}, []);
const removeEmail = React.useCallback((event) => {
const index = parseInt(event.target.dataset.index, 10);
setInviteEmails((inviteEmails) => {
const newInviteEmails = [...inviteEmails];
newInviteEmails.splice(index, 1);
return newInviteEmails;
});
}, []);
const addEmail = React.useCallback(
() => setInviteEmails((inviteEmails) => [...inviteEmails, ""]),
[]
);
return (
<div>
{inviteEmails.map((email, index) => (
<div key={index}>
<input
value={email}
data-index={index}
onChange={handleEmailChange}
placeholder="Enter email here..."
/>
<button onClick={removeEmail} data-index={index}>
×
</button>
</div>
))}
<button onClick={addEmail}>Add email</button>
<pre>{JSON.stringify(inviteEmails.filter(Boolean), null, 2)}</pre>
</div>
);
}

how to store value of 2 inputs dynamically generated in state at once

i am using react.i have 2 inputs that by clicking a button dynamically ganerats.
this the code:
useEffect(() => {
const myFields = fieldsArray.map((field) => {
return (
<div key={field} className="col-12 m-auto d-flex">
<input id={field} name={`question${field}`} onChange={handleChangeFields} placeholder='سوال' className="form-control col-4 m-2" />
<input id={field} name={`answer${field}`} onChange={handleChangeFields} placeholder='جواب' className="form-control col-4 m-2" />
</div>
)
})
setFields(myFields)
}, [fieldsArray])
i need to save value of 2 inputs together in an opjects like this :
[{question : '',answer : ''}]
and the new 2 input's value are going to update the above array like this:
[{question : '',answer : ''}, {question : '',answer : ''}]
i can save each input's value separately like this :
const handleChangeFields = (e) => {
const { name, value } = e.target
setFieldsValue([...fieldsValue, { [name]: value }])
}
but i want each 2 input's value together
i need to save each 2 inputs together.
how do i do that?
This is a general example of how I think you can tie in what you're currently doing, and add in as little new code as possible. This logs the new data state when the button is clicked.
Have two states. One for collecting the updated form information (form), and another for the combined form data array (data).
Have a form with a single onChange listener (event delegation) so you can catch events from all the inputs as they bubble up the DOM. That listener calls the handleChange function which updates the form state.
Have a button with an onClick listener that calls handleClick which takes the current form state and adds it to the data state.
I would be a bit wary of storing JSX in state. You should have that map in the component return and only updating the actual field data with basic information.
One final issue - your inputs cannot have the same id. ids must be unique. I'd get rid of them altogether.
const { useEffect, useState } = React;
function Example() {
// Initialise two states. `data` is an array
// and `form` is an object
const [ data, setData ] = useState([]);
const [ form, setForm ] = useState({});
// Add the form data to the `data` array
function handleClick() {
setData([ ...data, form ]);
}
// We're using event delegation so we need to
// check what element we changed/clicked on
// - in this case the INPUT element. We can then
// update the form state with the name/value pair of that input
function handleChange(e) {
const { nodeName, name, value } = e.target;
if (nodeName === 'INPUT') {
setForm({ ...form, [name]: value });
}
}
// Just logs the data state after a change
useEffect(() => console.log(data), [data]);
return (
<form onChange={handleChange}>
<input name="question" type="text" />
<input name="answer" type="text" />
<button
type="button"
onClick={handleClick}
>Update form
</button>
</form>
);
};
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
this problem solved for me :
function App() {
const [results, setResults] = useState([{ question: "", answer: "" }])
// handle input change
const handleInputChange = (e, index) => {
const { name, value } = e.target
const list = [...results]
list[index][name] = value
setResults(list)
}
// handle click event of the Remove button
const handleRemoveClick = (index) => {
const list = [...results]
list.splice(index, 1)
setResults(list)
}
// handle click event of the Add button
const handleAddClick = () => {
setResults([...results, { question: "", answer: "" }])
}
// handle submit to servers
const handlesubmit = () => {
// axios
console.log(results)
}
return (
<div className="App">
{results?.map((result, index) => {
return (
<div key={index}>
<input
name="question"
value={result.question}
onChange={(e) => handleInputChange(e, index)}
/>
<input
name="answer"
value={result.answer}
onChange={(e) => handleInputChange(e, index)}
/>
{results.length !== 1 && (
<button onClick={() => handleRemoveClick(index)}>
Remove
</button>
)}
{results.length - 1 === index && (
<button onClick={handleAddClick}>add</button>
)}
</div>
)
})}
<div style={{ marginTop: 20 }}>{JSON.stringify(results)}</div>
<button onClick={handlesubmit}>submit</button>
</div>
)
}
export default App

Sending parameter with function to child component and back to parent component

I've got a custom component called InputWithButton that looks like this:
const InputWithButton = ({ type = "text", id, label, isOptional, name, placeholder = "", value = "", showPasswordReset, error, isDisabled, buttonLabel, handleChange, handleBlur, handleClick }) => (
<StyledInput>
{label && <label htmlFor="id">{label}{isOptional && <span className="optional">optioneel</span>}</label>}
<div>
<input className={error ? 'error' : ''} type={type} id={id} name={name} value={value} placeholder={placeholder} disabled={isDisabled} onChange={handleChange} onBlur={handleBlur} autoComplete="off" autoCorrect="off" />
<Button type="button" label={buttonLabel} isDisabled={isDisabled} handleClick={() => handleClick(value)} />
</div>
{error && <Error>{Parser(error)}</Error>}
</StyledInput>
);
export default InputWithButton;
Button is another component and looks like this:
const Button = ({ type = "button", label, isLoading, isDisabled, style, handleClick }) => (
<StyledButton type={type} disabled={isDisabled} style={style} onClick={handleClick}>{label}</StyledButton>
);
export default Button;
I'm using the InputWithButton component in a parent component like this:
render() {
const { name } = this.state;
return (
<React.Fragment>
<InputWithButton label="Name" name="Name" buttonLabel="Search" value={name} handleChange={this.handleChange} handleClick={this.searchForName} />
</React.Fragment>
);
}
If the button is clicked, the searchForName function is called:
searchForName = value => {
console.log(value); //Input field value
}
This is working but I want to add another parameter to it but this time, a parameter that comes from the parent component
// handleClick={() => this.searchForName('person')}
<InputWithButton label="Name" name="Name" buttonLabel="Search" value={name} handleChange={this.handleChange} handleClick={() => this.searchForName('person')} />
The output in searchForName is now 'person' instead of the value.
I thought I could fix this with the following code:
searchForName = type => value => {
console.log(type); //Should be person
console.log(value); //Should be the value of the input field
}
However this approach doesn't execute the function anymore.
How can I fix this?
EDIT: Codepen
I would try handleClick={this.searchForName.bind(this, 'person')}, please let me know if it'll work for you.
EDIT:
I changed fragment from your codepen, it's working:
searchName(key, value) {
console.log(key);
console.log(value);
}
render() {
const { name } = this.state;
return (
<InputWithButton name="name" value={name} buttonLabel="Search" handleChange={this.handleChange} handleClick={this.searchName.bind(this, 'person')} />
)
}
as I suspected, just pass it an object and make sure you're accepting the argument in your handleClick function
handleClick={value => this.searchName({value, person: 'person'})}
or more verbose - without the syntactic sugar
handleClick={value => this.searchName({value: value, person: 'person'})}
then you can get at it with value.person
full codepen here
Hope this helps

Categories