Cannot remove inputs array with filter - reactjs

I am trying to remove an input field with filter function but it's not working.
In the following code add operation works fine but remove operation is not working properly ,it is not removing the corresponding element.Another problem the values on the inputs fields not present when the component re-renders.so experts guide me how i can achieve removing the corresponding row when the remove button is clicked and the input values should not be reset when the component re-renders
So when I refresh the page and click to remove an input it will clear all other input data. How can I fix this problem ?
Update adding full component in question:
const Agreement = (props) => {
const { agreement, editable, teamData, teamId, fetchTeamData } = props;
const [editing, setEditing] = useState(false);
const [title, setTitle] = useState("");
const [showErrors, setShowErrors] = useState(false);
const [errorsArr, setErrorsArr] = useState();
const initialFormState = {
rule_0: teamData.rules.rule_0,
rule_1: teamData.rules.rule_1,
rule_2: teamData.rules.rule_2,
rule_3: teamData.rules.rule_3,
creator: teamData.User.public_user_id,
};
const [updateTeamData, setUpdateTeamData] = useState(initialFormState);
const [inputs, setInputs] = useState(
teamData.rules.map((el) => ({
...el,
guid: uuidV4(),
}))
);
const handleChange = (event) => {
const { name, value } = event.target;
// Update state
setUpdateTeamData((prevState) => ({
...prevState,
[name]: value,
}));
};
// Add more input
const addInputs = () => {
setInputs([...inputs, { name: `rule_${inputs.length + 1}` }]);
};
// handle click event of the Remove button
const removeInputs = (index) => {
const newList = inputs.filter((item, i) => index !== i); // <-- compare for matching index
setInputs(newList);
};
const clearInput = (dataName) => {
setUpdateTeamData((prevState) => {
delete prevState[dataName];
return {
...prevState,
};
});
};
const handleSubmit = async (event) => {
event.preventDefault();
setEditing(false);
// Send update request
const res = await axios.put(`/api/v1/teams/team/${teamId}`, updateTeamData);
// If no validation errors were found
// Validation errors don't throw errors, it returns an array to display.
if (res.data.validationErrors === undefined) {
// Clear any errors
setErrorsArr([]);
// Hide the errors component
setShowErrors(false);
// Call update profiles on parent
fetchTeamData();
} else {
// Set errors
setErrorsArr(res.data.validationErrors.errors);
// Show the errors component
setShowErrors(true);
}
};
const handleCancel = () => {
setEditing(false);
};
useEffect(() => {
if (agreement === "default") {
setTitle(defaultTitle);
// setInputs(teamData.rules);
} else {
setTitle(agreement.title ?? "");
}
}, [agreement, teamData]);
// console.log("teamData.rules", teamData);
console.log("inputs", inputs);
return (
<div className="team-agreement-container">
{!editing && (
<>
<h4 className="team-agreement-rules-title">{title}</h4>
{editable && (
<div className="team-agreement-rules">
<EditOutlined
className="team-agreement-rules-edit-icon"
onClick={() => setEditing(true)}
/>
</div>
)}
{teamData.rules.map((rule, index) => (
<div className="team-agreement-rule-item" key={`rule-${index}`}>
{rule ? (
<div>
<h4 className="team-agreement-rule-item-title">
{`Rule #${index + 1}`}
</h4>
<p className="team-agreement-rule-item-description">
- {rule}
</p>
</div>
) : (
""
)}
</div>
))}
</>
)}
{/* Edit rules form */}
{editing && (
<div className="team-agreement-form">
{showErrors && <ModalErrorHandler errorsArr={errorsArr} />}
<h1>Rules</h1>
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={data.guid}>
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
{inputs.length < 4 && (
<div className="team-agreement-add-rule">
<button type="submit" onClick={addInputs}>
<Add />
</button>
</div>
)}
<div className="div-button">
<button className="save-button" onClick={handleSubmit}>
Save
</button>
<button className="cancel-button" onClick={handleCancel}>
Cancel
</button>
</div>
</div>
)}
</div>
);
};
export default Agreement;
When i do console.log(inputs) this is the data that I got:
0: 0: "t" 1: "e" 2: "s" guid: "e18595a5-e30b-4b71-8fc2-0ad9c0e140b2"
proto: Object 1: 0: "d" 1: "a" 2: "s" 3: "d" 4: "a" 5: "s" guid: "537ca359-511b-4bc6-9583-553ea6ebf544" ...

Issue
The issue here is that you are using the array index as the React key. When you mutate the underlying data and reorder or add/remove elements in the middle of the array then the elements shift around but the React key previously used doesn't move with the elements.
When you remove an element then all posterior elements shift forward and the index, as key, remains the same so React bails on rerendering the elements. The array will be one element shorter in length and so you'll see the last item removed instead of the one you actually removed.
Solution
Use a React key that is intrinsic to the elements being mapped, unique properties like guids, ids, name, etc... any property of the element that guarantees sufficient uniqueness among the dataset (i.e. the siblings).
const [inputs, setInputs] = useState(teamData.rules);
const removeInputs = (index) => {
// compare for matching index
setInputs(inputs => inputs.filter((item, i) => index !== i));
};
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={data.id}> // <-- use a unique property
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
If your teamData.rules initial state value doesn't have any unique properties to use then you can map this to a new array and add a sufficient id property.
const [inputs, setInputs] = useState(teamData.rules.map(el => ({
...el,
guid: generateId()***,
})));
*** this is a function you need to define yourself, or import from a module like uuid***
import { v4 as uuidV4 } from 'uuid';
...
const [inputs, setInputs] = useState(teamData.rules.map(el => ({
...el,
guid: uuidV4(),
})));
// Add more input
const addInputs = () => {
setInputs(inputs => [
...inputs,
{
name: `rule_${inputs.length + 1}`,
guid: uuidV4();
},
]);
};
Then when mapping use the guid property.
<div className="agreement-form-grid" key={data.guid}>

The issue is because you are trying to compare index with array item in filter method. You should use the second argument in filter which denotes the array index of the current iterating item
const removeInputs = (index) => {
const newList = inputs.filter((item,i) => index !== i);
setInputs(newList);
};

That's your solution, you are trying with item but you are comparing it with index that's wrong. You should do it like this,
const newList = inputs.filter((item, key) => index !== key);

Related

How can i add and remove components dinamically?

I want to add and remove the components dynamically, so far just i can add, but when i tried to remove it remove too weird, lets say i dont want to remove just i would like to hide the components
import {
MinusCircleOutlined,
PlusOutlined,
} from '#ant-design/icons'
import { useState } from "react"
const MyInput = ({ index, removeInput }) => {
return (<div >
<Input placeholder="Email address" />
<MinusCircleOutlined className="icon-left" onClick={() => { removeInput(index) }} />
</div>
)
}
const MyComponent = ({ }) => {
const [form] = Form.useForm()
const [index, setIndex] = useState(0)
const [inputsFields, setInputsFields] = useState([])
const [hiddenFields, setHiddenFields] = useState([])
const AddInput = () => {
const newInviteField = <MyInput index={index} removeInput={removeInput} />
setInputsFields([...inputsFields, newInviteField])
const newIndex = index + 1
setIndex(newIndex)
}
const removeInput = (currentIndex) => {
let a = hiddenFields
a.push(currentIndex)
setHiddenFields([...a])
}
return (
<Card>
<Form form={form} layout="vertical">
<Form.Item className='form-item item-container'>
{inputsFields.map((item, index) => !hiddenFields.includes(index) && <div key={index}>{item}</div>)}
</Form.Item>
<Form.Item >
<a href="#" onClick={AddInput}>Add</a>
</Form.Item>
</Form>
</Card>)
}
i tried to filter by the index, just showing the indexes does not into the hidden array !hiddenFields.includes(index)
the problem is when i am deleting, sometimes it is not deleting, sometimes other component is deleting
You should never use an array method index as key, if you modify the array. It has to be unique. When you delete element with index 2, the index 3 becomes index 2. This should not happend. You should not change the key prop value
The solution:
keep information about the inputs in the state, not the inputs itself.
// keep necessarry information for each input here.
// Like id, name, hidden, maybe what to render. Whatever you want
const [inputsFields, setInputsFields] = useState([{
id: 'name',
hidden: false
}])
// and map them
inputsFields.map(element => !element.hidden && <Input key={element.id} />)
When each element has unique id, you will delete the element with that id, not with the array map index
If you do not need that much info. Just make array of numbers in that state,
const counter = useRef(1)
const [inputsFields, setInputsFields] = []
const AddInput = () => {
counter.current += 1
setInputsFields(oldInputs => [...oldInputs, counter.current])
}
// and render them:
inputsFields.map(element => <Input key={element} />)

how to update input defaultvalue using useRef (after the initial render?)

const Component = ()=>{
const [list, setList] = useState(getLocalStorage());
const [isEditing, setIsEditing] = useState(false);
const [itemToEdit, setItemToEdit] = useState();
const refContainer = useRef(null);
const putLocalStorage = () => {
localStorage.setItem("list", JSON.stringify(list));
};
const editItem = (id) => {
refContainer.current.focus();
setItemToEdit(() => {
return list.find((item) => item.id === id);
});
setIsEditing(true);
};
const handleSubmit = (e)=>{
e.preventDefault();
let nameValue = refContainer.current.value;
if (isEditing){
setList(list.map((item)=>{
if (item.id === itemToEdit.id){
return {...item, name: nameValue};
}
else {
return item;
}
);
}
else {
let newItem = {
id: new Date().getItem().toString(),
name: nameValue,
}
setList([...list, newItem])
}
nameValue="";
setIsEditing(false);
}
useEffect(() => {
putLocalStorage();
}, [list]);
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" ref={refContainer} defaultValue={isEditing ? itemToEdit.name : ""}/>
<button type="submit">submit</button>
</form>
<div>
{list.map((item) => {
const { id, name } = item;
return (
<div>
<h2>{name}</h2>
<button onClick={() => editItem(id)}>edit</button>
<button onClick={() => deleteItem(id)}>
delete
</button>
</div>
);
})}
</div>
</div>
)
}
So this part:
<input type="text" ref={refContainer} defaultValue={isEditing ? itemToEdit.name : ""} />
I want to show to users what they are editing by displaying the itemToEdit on the input.
It works on the first time when the user clicks edit button
But after that, the defaultValue does not change to itemToEdit
Do you guys have any idea for the solution?
(i could use controlled input instead, but i want to try it with useRef only)
Otherwise, placeholder will be the only solution...
The defaultValue property only works for inicial rendering, that is the reason that your desired behavior works one time and then stops. See a similar question here: React input defaultValue doesn't update with state
One possible solution still using refs is to set the itemToEdit name directly into the input value using ref.current.value.
const editItem = (id) => {
refContainer.current.focus();
setItemToEdit(() => {
const item = list.find((item) => item.id === id);
refContainer.current.value = item.name;
return item;
});
setIsEditing(true);
};

React dynamic nested form input

Im a newbie in React and Im creating a simple form that sends data to DB. I made it work almost as I wanted, the only problem is that I dont know how to update the state which has an array inside.
The idea is to make a form so I can add recipes which include the whole recipe data that I map through to render each recipe. In the data object I need simple strings most of the time but then I need also three arrays or objects, I prefer the arrays in this case.
I found many solutions for class components but still I could figure out how to update the arrays. I even figured out how to update one array from a string input separated only with commas, then .split(', ') and .trim() and map() through but I could not setFormFields({}) at two places at the same time since the createRecipe() is async. The split just did not happen before the array was sent to the DB as a string. Thats why I dont put the whole code here.
I will simplify the code to make you see clear.
const defaultFormFields = {
title: '',
imageUrl: '',
leadText: '',
};
const NewRecipeForm = () => {
const [formFields, setFormFields] = useState(defaultFormFields);
const { title, imageUrl, leadText } = formFields;
const [ingredients, setIngredients] = useState([])
const handleFormFieldsChange = (event) => {
setFormFields({ ...formFields, [event.target.name]: event.target.value })
}
const handleIngredientsChange = ( event) => {
**// here I need help**
setIngredients()
}
const addIngredient = () => {
setIngredients([...ingredients, ''])
}
const removeIngredient = (index) => {
**// here I need help**
}
const createRecipe = async (event) => {
event.preventDefault()
// addRecipe sends the object to Firestore DB
addRecipe('recipes', url, formFields)
resetFormFields()
}
const resetFormFields = () => {
setFormFields(defaultFormFields);
};
return (
<NewRecipeFormContainer>
<h1>New recipe</h1>
<form onSubmit={createRecipe}>
<h1>Title</h1>
<input label='Title' placeholder='Recipe title' name='title' value={title} onChange={handleChange} />
<input label='imageUrl' placeholder='imageUrl' name='imageUrl' value={imageUrl} onChange={handleFormFieldsChange} />
<input label='leadText' placeholder='leadText' name='leadText' value={leadText} onChange={handleFormFieldsChange} />
<h1>Ingredients</h1>
**// here I need help ?**
{
ingredients.map((ingredient, index) => {
return (
<div key={index}>
<input label='Ingredience' placeholder='Ingredience' name='ingredient' value={ingredient.ingredient} onChange={handleChange} />
**// here I need help ?**
<button onClick={removeIngredient} >remove</button>
</div>
)
})
}
<button onClick={addIngredient} >add</button>
</form>
<Button onClick={createRecipe}>ODESLAT</Button>
</NewRecipeFormContainer>
)
}
I will appreciate any hint or help. Ive been totally stuck for two days. Thank you!
Here's an example of how to update a single element in a list.
const updateSingleItemInList = (index, update) => {
setList(list.map((l, i) => i === index ? update : l));
};
const add = (element) => setList([...list, element]);
Try simplifying your state first:
const [ingredients, setIngredients] = useState([]);
const [tips, setTips] = useState([]);
Then it becomes simple to write the handlers:
const updateIngredient = (index, text) => {
setIngredients(list.map((ing, i) => i === index ? text : ing));
};
const addIngredient = () => setIngredients([...ingredients, ""]);
Then you can create the form object when the user wants to submit:
addRecipe('recipes', url, {
ingredients: ingredients.map(i => ({ingredients: i})),
// etc.
});
Put it all together and here is the minimum viable example of a component that manages a dynamic number of form elements (tested, works):
export const TextBody = () => {
const [list, setList] = useState([{ name: "anything" }]);
const add = () => setList(l => [...l, { name: "" }]);
const remove = i => setList(l => [...l.slice(0, i), ...l.slice(i + 1)]);
const update = (i, text) => setList(l => l.map((ll, ii) => (ii === i ? { name: text } : ll)));
return (
<>
<TouchableOpacity onPress={add}>
<Text text="add" />
</TouchableOpacity>
{list.map((l, i) => (
<>
<Text text={JSON.stringify(l)} />
<TouchableOpacity onPress={() => remove(i)}>
<Text text="remove" />
</TouchableOpacity>
<Input onChange={c => update(i, c.nativeEvent.text)} />
</>
))}
</>
);
};
You can return those CRUD functions and the state from a custom hook so you only have to write this once in a codebase.
Edit: Just for fun, here's the same component with a reusable hook:
const useListOfObjects = (emptyObject = {}, initialState = []) => {
const [list, setList] = useState(initialState);
const add = () => setList(l => [...l, emptyObject]);
const remove = i => setList(l => [...l.slice(0, i), ...l.slice(i + 1)]);
const update = (i, text, field) =>
setList(l => l.map((ll, ii) => (ii === i ? { ...ll, [field]: text } : ll)));
return {
list,
add,
remove,
update,
};
};
export const TextBody = () => {
const { list, add, remove, update } = useListOfObjects({ name: "", id: Math.random() });
return (
<>
<TouchableOpacity onPress={add}>
<TextBlockWithShowMore text="add" />
</TouchableOpacity>
{list.map((l, i) => (
<React.Fragment key={`${l.id}`}>
<TextBlockWithShowMore text={JSON.stringify(l)} />
<TouchableOpacity onPress={() => remove(i)}>
<TextBlockWithShowMore text="remove" />
</TouchableOpacity>
<Input onChange={c => update(i, c.nativeEvent.text, "name")} />
</React.Fragment>
))}
</>
);
};

How to properly use state hook with useSelector?

I have two react-select elements in my app and the 2nd one should be populated with options based on the selection of the first. It works but when the Select #1 is cleared, #2 is not being reset.
So I decided to implement useState to help me update/reset the options for Select #2 but it's acting wonky. Am I missing something or is there a better way to accomplish this?
export const dataViewPage = ({
getData,
loadLocationsByType,
classes,
}) => {
const [stateValue, setStateValue] = useState('');
const [store, setStore] = useState('');
const [storeOptions, setStoreOptions] = useState([]); // Constant to populate options for Select #2
const locationType = 'store';
const Input = styled('input')({ display: 'none' });
/* Returns locations in selected state in array of objects with location_id and name */
const locations = useSelector((state) =>
getFilteredLocations(state, stateValue, locationType)
);
useEffect(() => { // Fetches locations data on state change
stateValue &&
loadLocationsByType({ locationType: locationType, state: stateValue });
}, [stateValue, loadLocationsByType]);
useEffect(() => { // Sets store options in expected format
if (locations) {
setStoreOptions(
convertObjArrayToSelectOptions(locations, 'location_id', 'name')
);
} else { // Reset if no state is selected?
setStoreOptions([]);
}
}, [locations, stateValue]);
}
/* Main input functions */
const handleStateChange = (e) => {
let stateEntered = e ? e.value : '';
setStateValue(stateEntered);
};
const handleStoreChange = (e) => {
let selectedStore = e ? e.value : '';
setStore(`T${selectedStore}`);
console.log(selectedStore);
};
return (
<div>
<div>
<p>Enter or select state</p>
<Select
classes={classes}
placeholder="Select state"
options={states}
onChange={handleStateChange}
isClearable
/>
</div>
<div>
<p>Select store:</p>
<Select
options={storeOptions}
isClearable
onChange={handleStoreChange}
/>
</div>
<label>
<Input type="submit" />
<Button
onClick={handleSubmit}
>
View data
</Button>
</label>
</div>
);
};

Decrease value in inputs - reactjs

I have an input whose value is 4 or it can be any one and when I press the button it is generating another dynamic input, what I need is that each time a dynamic input is generated the value of each input is subtracted by -1 until it is in 1.
I have not been able to make it work as I need it, if someone can help me I would be very grateful, I have reviewed several examples but I have not been able to make it work, any help is welcome.
import { useState } from "react";
const defaultState = {
nombre: 4
};
function Row({ onChange, onRemove, nombre }) {
return (
<div>
<input
value={nombre}
onChange={e => onChange("nombre", e.target.value)}
placeholder="Decrementar"
/>
<button onClick={onRemove}>Eliminar</button>
</div>
);
}
export default function Pruebas() {
const [rows, setRows] = useState([defaultState]);
const handleOnChange = (index, name, value) => {
const copyRows = [...rows];
copyRows[index] = {
...copyRows[index],
[name]: value
};
setRows(copyRows);
};
const handleOnAdd = () => {
setRows(rows.concat(defaultState));
};
const handleOnRemove = index => {
const copyRows = [...rows];
copyRows.splice(index, 1);
setRows(copyRows);
};
return (
<div className="App">
{rows.map((row, index) => (
<Row
{...row}
onChange={(name, value) => handleOnChange(index, name, value)}
onRemove={() => handleOnRemove(index)}
key={index}
/>
))}
<button onClick={handleOnAdd}>-</button>
</div>
);
}
When you concatenate a new row make sure to decrement the field nombre's value see for a full example : https://codesandbox.io/s/stack-over-inc-3nixd

Resources