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} />)
Related
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
I'm trying to get value from onChange using setState but for some reason when I write text on input I get an error like Axis.map is not a function
Also,I'd like to delete Axisdata singly from the last one using splice or pop but whenever I click the delete button the Axis data disappeared except the first one.
Set Elements
const SetElements = ({
...
}) => {
const [Axis, setAxis] = useState([]);
const AxisHandler = e => {
setAxis([
...Axis,
{
label: "",
data: "",
backgroundColor: "",
},
]);
};
const deleteAxis = () => {
setAxis(Axis.splice(-1, 1));
};
return (
<>
<button onClick={AxisHandler}>add Line</button>
{Axis.length !== 1 && (
<button onClick={deleteAxis}>delete Line</button>
)}
{Axis.map((element, index) => (
<>
<AppendingAxis
Axis={Axis}
setAxis={setAxis}
element={element}
index={index}
/>
</>
))}
</>
)
AppendingAxis
const AppendingAxis = ({
index,
setAxis,
Axis,
}) => {
console.log(Axis);
return (
<AxisSetting>
<h4>{index + 2}Y Axis setting</h4>
<span>
<input
placeholder={index + 2 + "setting"}
type="textarea"
onChange={e => setAxis((Axis[index].label = e.target.value))}
/>
</span>
The issue is state mutation in the AppendingAxis component.
onChange={e => setAxis((Axis[index].label = e.target.value))}
You should shallow copy state, and nested state, then update specific properties.
onChange={e => setAxis(Axis => Axis.map((el, i) => i === index
? {
...el,
label: e.target.value
}
: el,
)}
I'm not a fan of passing the state updater function on to children as this make it the child component's responsibility to maintain your state invariant. I suggest moving this logic into the parent component so it can maintain control over how state is updated.
SetElements parent
const changeHandler = index => e => {
const { value } = e.target;
setAxis(Axis => Axis.map((el, i) => i === index
? {
...el,
label: value
}
: el,
);
};
...
<AppendingAxis
Axis={Axis}
onChange={changeHandler(index)}
/>
AppendingAxis child
const AppendingAxis = ({ Axis, onChange }) => {
console.log(Axis);
return (
<AxisSetting>
<h4>{index + 2}Y Axis setting</h4>
<span>
<input
placeholder={index + 2 + "setting"}
type="textarea"
onChange={onChange}
/>
</span>
And for completeness' sake, your delete handler looks to also have a mutation issue.
const deleteAxis = () => {
setAxis(Axis.splice(-1, 1));
};
.splice mutates the array in-place and returns an array containing the deleted elements. This is quite the opposite of what you want I think. Generally you can use .filter or .slice to generate new arrays and not mutate the existing one.
const deleteAxis = () => {
setAxis(Axis => Axis.slice(0, -1)); // start at 0, end at second to last
};
This is happening because of this line:
onChange={e => setAxis((Axis[index].label = e.target.value))}
Create a function:
const handleAxisChange = (e, index) => {
Axis[index].label = e.target.value;
setAxis(new Array(...Axis));
}
And then change set the onChange like this:
onChange={e => handleAxisChange(e, index)}
Your problem is because of you don't mutate state correctly. You should make a shallow copy of the state. You can change AppendingAxis to this code:
const AppendingAxis = ({
index,
setAxis,
Axis,
}) => {
console.log(Axis);
const onChange = (e,index)=>{
let copy = [...Axis];
copy[index].label = e.target.value;
setAxis(copy);
}
return (
<AxisSetting>
<h4>{index + 2}Y Axis setting</h4>
<span>
<input
placeholder={index + 2 + "setting"}
type="textarea"
onChange={e => onChange(e,index))}
/>
</span>
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);
What's the most "React"y way to build reusable, chainable filter components?
Let's say I have an input array:
[
{name: 'Generic t-shirt', size: 'xxl', available: 35},
{name: 'Generic t-shirt', size: 'md', available: 2},
{name: 'Generic t-shirt', size: 'sm', available: 5},
{name: 'Really awesome shirt', size: 'md', available: 0}
]
And a keyword search for the name, a dropdown for size, and a "sold out" boolean checkbox for availability.
Right now, I have the filtering code inside the rendering loop:
const [productsFilteredByFullText, setProductsFilteredByFullText] = useState;
const [productsFilteredBySize, setProductsFilteredBySize] = useState;
const [productsFilteredByAvailability, setProductsFilteredByAvailability] = useState;
const searchResults = useMemo(() => {
let filteredProducts = eventsFromAPI; // array of products as input
filteredProducts = filteredEvents.filter(product => fullTextFilter(product));
filteredProducts = filteredEvents.filter(product => sizeFilter(product));
filteredProducts = filteredEvents.filter(product => availabilityFilter(product));
return filteredProducts;
}, [productsFilteredByFullText, productsFilteredBySize, productsFilteredByAvailability]);
And the UI inside JSX:
<div>
// Fulltext search
<input
type="text"
placeholder="Keyword search"
value={searchTerm}
onChange={fullTextHandler}
/>
// Size dropdown
<Dropdown
options={allAvailableSizes}
onChange={sizeHandler}
/>
// Sold out checkbox
<input
name="soldout"
type="checkbox"
checked={productsFilteredByAvailability}
onChange={availabilityHandler}
/>
<h1>Results</h1>
{filteredProducts.map(item => (
<Product item={item} />
))}
</div>
This works, but is not very reusable at all. Let's say I have another product category, scarves, that are all one size, but I still want to be able to re-use the filter component and logic for name and availability.
Is there a way to modularize/componentize BOTH the filter logic AND the presentation JSX into separate filter components, and be able to chain them together arbitrarily? Something like this pseuocode:
<TShirtList>
<NameFilter/>
<SizeFilter/>
<AvailabilityFilter/>
</TShirtList>
<ScarfList>
<NameFilter/>
<AvailabilityFilter/>
</ScarfList>
<ServicesList>
<NameFilter/>
</ServicesList>
So that each filter is its own component, able to be inserted into any array of products anywhere? Like how can a React component also provide its own logic/functions that other components can use (product array in, filtered product array + JSX UI out, but chainable).
Is that even the right way to think about this problem...? I'm confused about how to best build this architecturally.
Answer to this question
What's the most "React"y way to build reusable, chainable filter
components?
I suggest separate display & data logic by using hooks. Move all data related logic into a hook, and import that hook to a react component.
// useProducts.js
const useProducts = () => {
const [products, setProducts = useState([]);
const [filters, setFilters] = useState({}); // eg. filters = { size: 'large', availability: 'instock' }
// Get all products on initial load
useEffect(() => {
fetch(....).then(response => setProducts(response.data));
}, []);
// function to set filter.
const setFilter = (name, value) => {
setFilters({ ...filters, [name]: value });
}
// Loop filters and filter product
const filteredProducts = [...products];
for (const [key, value] of Object.entries(filters)) {
// Get list of products from somewhere.
filteredProducts = filterProducts.filter(p => p[key] === value);
}
return [filteredProducts, { setFilter }];
}
// Product List
import React from 'react';
import useProducts from './useProducts';
const ProductList = (props) => {
const [products, { setFilter }] = useProducts();
return (
<div>
<div className="toolbar">
<NameFilter onChange={value => setFilter('name', value)} />
<SizeFilter onChange={value => setFilter('size', value)} />
<AvailabilityFilter onChange={value => setFilter('availability', value)} />
</div>
<div>
{products.map(p => <ProductItem {...p} />}
</div>
</div>
);
}
Additionally, you can even modularize it more by import { setFilter } in the filter component itself. And you can remove the 'onChange' from the ProductList.
const NameFilter = () => {
const [, { setFilter }] = useProducts();
return (
<input
type="text"
onChange={e => setFilter('name', e.target.value)}
/>
);
}
// And now, we can remove onChange from each filter in Product List component
<div>
<div className="toolbar">
<NameFilter />
<SizeFilter />
<AvailabilityFilter />
</div>
<div>
{products.map(p => <ProductItem {...p} />}
</div>
</div>
*Note: Above code is just psuedo, it just shows the idea.
Any issues with conditionally filtering?
const searchResults =(text, size) => {
let filteredProducts = eventsFromAPI; // array of products as input
filteredProducts = text ? filteredEvents.filter(product => fullTextFilter(text)) : filteredProducts;
filteredProducts = size ? filteredEvents.filter(product => sizeFilter(product)) : filteredProducts
filteredProducts = filteredEvents.filter(product => availabilityFilter(product));
return filteredProducts;
};
size can be boolean.
can also be ... (psuedo code)
const Search =({filter = []}) => {
let filteredProducts = eventsFromAPI; // array of products as input
filteredProducts = filter.indexOf('something') > -1 ? filteredEvents.filter(product => fullTextFilter(text)) : filteredProducts;
filteredProducts = filter.indexOf('size') > -1 ? filteredEvents.filter(product => sizeFilter(product)) : filteredProducts
filteredProducts = filteredEvents.filter(product => availabilityFilter(product));
return filteredProducts;
};
then u can call search({ filter: ['size', 'availability']}) or if you made it in to an component then <Search filter={['size', 'availability']} products={products} />
or even
const Search = ({ size = null, text = '', availability = true}) => {
... conditionally filtering
return //whatever product u render
}
I'm pretty new to using hooks and functional components.
I have a Filtered List. When I try to update the filter, it will use the last filter state instead of the new one. I must be missing some render/state change orders, but I can't seem to figure out what it is.
I appreciate any help I can get :)
Pseudo code below:
export default function TransferList(props) {
const [wholeList, setWholeList] = React.useState([]);
const [filteredList, setFilteredList] = React.useState([]);
const [filter, setFilter] = React.useState([]);
return (
<>
<TextField
value={filter}
onChange={(e) => {
// Set filter input
setFilter(e.target.value);
// Filter the list
let filtered = wholeList.filter(
(item) => item.indexOf(filter) !== -1
);
setFilteredList(filtered);
}}
/>
<List>
{filteredList.map((item) => (
<ListItem>Item: {item}</ListItem>
))}
</List>
</>
);
}
Inside onChange you should use save the value in a constant, filter will not update just after setFilter(filterValue) as this is an async operation.
<TextField
value={filter}
onChange={e => {
const filterValue = e.target.value;
// Set filter input
setFilter(filterValue);
// Filter the list
let filtered = wholeList.filter(item => item.indexOf(filterValue) !== -1);
setFilteredList(filtered);
}}
/>;
State updates are asynchronous and hence the filter state update doesn't reflect immediately afterwords
You must store the new filter values and set the states based on that
export default function TransferList(props) {
const [wholeList, setWholeList] = React.useState([]);
const [filteredList, setFilteredList] = React.useState([]);
const [filter, setFilter] = React.useState([]);
return (
<>
<TextField value={filter} onChange={(e) => {
// Set filter input
const newFilter = e.target.value;
setFilter(newFilter)
// Filter the list
let filtered = wholeList.filter(item => item.indexOf(newFilter) !== -1)
setFilteredList(filtered)
}} />
<List>
{filteredList.map(item => <ListItem>Item: {item}</ListItem>)}
</List>
</>
)
}
So it turns out I had to give the initial filtered list the entire unfiltered list first. For some reason that fixes it.
const [filteredList, setFilteredList] = React.useState(props.wholeList);
I initially wanted an empty filter to display nothing, but I may have to settle for showing the entire list when the filter is empty
Order your code to be increase readability
In clean code
Main changes:
Use destructuring instead of props
Out the jsx from the html return to increase readability
Use includes instead of indexOf
Add key to the list
export default function TransferList({ wholeList }) {
const [filteredList, setFilteredList] = React.useState(wholeList);
const [filter, setFilter] = React.useState([]);
const handleOnChange = ({ target }) => {
setFilter(target.value);
const updatedFilteredList = wholeList.filter(item => item.includes(target.value));
setFilteredList(updatedFilteredList);
}
const list = filteredList.map(item => {
return <ListItem key={item}>Item: {item}</ListItem>
});
return (
<>
<TextField value={filter} onChange={handleOnChange} />
<List>{list}</List>
</>
);
}
and I have a filter component the do the filter list inside.
import React, { useState } from 'react';
function FilterInput({ list = [], filterKeys = [], placeholder = 'Search',
onFilter }) {
const [filterValue, setFilterValue] = useState('');
const updateFilterValue = ev => {
setFilterValue(ev.target.value);
const value = ev.target.value.toLowerCase();
if (!value) onFilter(list);
else {
const filteredList = list.filter(item => filterKeys.some(key => item[key].toLowerCase().includes(value)));
onFilter(filteredList);
}
}
return (
<input className="filter-input" type="text" placeholder={placeholder}
value={filterValue} onChange={updateFilterValue} />
);
}
export default FilterInput;
the call from the father component look like this
<FilterInput list={countries} filterKeys={['name']} placeholder="Search Country"
onFilter={filterCountries} />
this is in my corona app.. you can look on my Github.
and see how I build the filter
(https://github.com/omergal99/hello-corona)