Can't render elements from mutated Array - React - javascript

So, the problem I've faced is that when I click delete button at any index it just deletes last element
for example, if I press first delete button, it should remove first input and the delete button, but what it does is that it deletes last element only... What could be wrong?
function App() {
const [names, setNames] = React.useState([
"First",
"Second",
"third",
"fourth"
]);
const onChange= (index: number, editedName: string) => {
const mutatedNames = [...names];
mutatedNames[index] = editedName;
setNames(mutatedNames);
};
function onDelete(index: number) {
const nameArr = [...names];
nameArr.splice(index, 1);
setNames(nameArr);
}
return (
<div>
{names.map((name, index) => (
<ChildComponent
key={index}
name={name}
index={index}
onChange={onChange}
onDelete={onDelete}
/>
))}
</div>
);
}
const Child = React.memo(
({ name, index, onChange, onDelete }) => {
return (
<div>
<input
onChange={(event) => onChange(index, event.target.value)}
/>
<button onClick={() => onDelete(index)}>delete</button>
</div>
);
}
);

You are using a partially controlled input, this is almost never a good idea.
Make it fully controlled like so:
<input
value={name}
onChange={(event) => onChange(index, event.target.value)} />
I suggest you read the official guidelines about Forms and Controlled Components and the article about the uncontrolled scenario.

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..."/>}

Reactjs: How to access state of child component.from a method in parent component that depends on state of child component

I need to access a method handleCancelEdit() defined in parent component. But, the matter here is that every child component will have its own cancelEdit state. Now, what is happening is, if I call handleCancelEdit() from one child component, every other of the same child components is taking the state and updating themselves(the method is not completely defined yet). That's why, I have defined the cancelEdit state in the child component, thinking that it belongs to this child component only.
Now, how do I make the handleCancelEdit() method make changes to the only child component which called it?
The parent:
function Parent() {
const handleCancelEdit = () => {
setCancelEdit(!cancelEdit); // defined in child component
setEdit(!edit); // defined in child component
...
};
return (
<div>
<ChildComponent
fieldName={"Email"}
value={email}
inputType={"text"}
placeHolder={"Enter email"}
name={"email"}
on_change={(e)=>setEmail(e.target.value)}
on_click={handleUserEmail}
/>
<ChildComponent
fieldName={"About"}
value={about}
inputType={"text"}
placeHolder={"Enter some details about yourself"}
name={"about"}
on_change={(e)=>setAbout(e.target.value)}
on_click={handleUserAbout}
/>
</div>
);
}
Child component:
function ChildComponent({fieldName, value, inputType, placeHolder, name, on_change, on_click}) {
const [ edit, setEdit ] = useState(false);
const [ cancelEdit, setCancelEdit ] = useState(false)
const isEdit = edit;
return (
<p>{fieldName}: {value === ''? (
<span>
<input type={inputType} placeholder={placeHolder}
name={name} onChange={on_change}
/>
<button type="submit" onClick={on_click}>Add</button>
</span>
) : ( !isEdit ? (<span> {value} <button onClick={e=>setEdit(!edit)}>Edit</button></span>) :
(<span>
<input type={inputType} value={value}
name={name} onChange={on_change}
/>
<button type="submit" onClick={on_click}>Save</button>
<button type="submit" onClick={handleCancelEdit}>Cancel</button>
</span>)
)}
</p>
);
};
I hope it could make it understandable that one child component should not make others to update. Now, how do I do it in this scenario?
EDIT
After making changes according to Linda Paiste:
The input field in the child component is not working even though the onChange in both parent and child is correct!
It is always more logical to pass state and data down rather than up. If the Parent needs to interact with the edit state then that state should live in the parent. Of course we want independent edit states for each child, so the parent can't just have one boolean. It needs a boolean for each child. I recommend an object keyed by the name property of the field.
In ChildComponent, we move isEdit and setEdit to props. handleCancelEdit is just () => setEdit(false) (does it also need to clear the state set by onChange?).
function ChildComponent({fieldName, value, inputType, placeHolder, name, onChange, onSubmit, isEdit, setEdit}) {
return (
<p>{fieldName}: {value === ''? (
<span>
<input type={inputType} placeholder={placeHolder}
name={name} onChange={onChange}
/>
<button type="submit" onClick={onSubmit}>Add</button>
</span>
) : ( !isEdit ? (<span> {value} <button onClick={() =>setEdit(true)}>Edit</button></span>) :
(<span>
<input type={inputType} value={value}
name={name} onChange={onChange}
/>
<button type="submit" onClick={onSubmit}>Save</button>
<button type="submit" onClick={() => setEdit(false)}>Cancel</button>
</span>)
)}
</p>
);
};
In Parent, we need to store those isEdit states and also create a setEdit function for each field.
function Parent() {
const [isEditFields, setIsEditFields] = useState({});
const handleSetEdit = (name, isEdit) => {
setIsEditFields((prev) => ({
...prev,
[name]: isEdit
}));
};
/* ... */
return (
<div>
<ChildComponent
fieldName={"Email"}
value={email}
inputType={"text"}
placeHolder={"Enter email"}
name={"email"}
onChange={(e) => setEmail(e.target.value)}
onSubmit={handleUserEmail}
isEdit={isEditFields.email}
setEdit={(isEdit) => handleSetEdit("email", isEdit)}
/>
<ChildComponent
fieldName={"About"}
value={about}
inputType={"text"}
placeHolder={"Enter some details about yourself"}
name={"about"}
onChange={(e) => setAbout(e.target.value)}
onSubmit={handleUserAbout}
isEdit={isEditFields.about}
setEdit={(isEdit) => handleSetEdit("about", isEdit)}
/>
</div>
);
}
You can clean up a lot of repeated code by storing the values as a single state rather than individual useState hooks. Now 5 of the props can be generated automatically just from the name.
function Parent() {
const [isEditFields, setIsEditFields] = useState({});
const [values, setValues] = useState({
about: '',
email: ''
});
const getProps = (name) => ({
name,
value: values[name],
onChange: (e) => setValues(prev => ({
...prev,
[name]: e.target.value
})),
isEdit: isEditFields[name],
setEdit: (isEdit) => setIsEditFields(prev => ({
...prev,
[name]: isEdit
}))
});
const handleUserEmail = console.log // placeholder
const handleUserAbout = console.log // placeholder
return (
<div>
<ChildComponent
fieldName={"Email"}
inputType={"text"}
placeHolder={"Enter email"}
onSubmit={handleUserEmail}
{...getProps("email")}
/>
<ChildComponent
fieldName={"About"}
inputType={"text"}
placeHolder={"Enter some details about yourself"}
onSubmit={handleUserAbout}
{...getProps("about")}
/>
</div>
);
}

React Controlled Form with Child /Parent component

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;
}),
);
};

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