Reputation: 308
,
I was working out with splice methods of js , but as it may seem it was not working exactly as it should remove any element from an array .
Currently its only deleting the last element from array even after providing the index value for it to delete from the array. in console.log i get the perfect output after deleting anything , but in UI part it does not update as it should , its only removing the last element from array even if i click on delete other item . How can i resolve this ?
Here's what i've tried so far :
const add_actions_options = [
{value : "Postback" , label:intl.formatMessage({ id: 'POSTBACK' })},
{value : "Uri" , label:intl.formatMessage({ id: 'URI' })}
]
const [ actions , setActions ] = useState<any | undefined>([{type : add_actions_options[0].value , label : "" , data : ""}])
const [selectOptions, setSelectOptions] = useState<any>(add_actions_options);
function addAction(){
if(actions.length < 4 ){
setSelectOptions([...add_actions_options])
setActions([...actions , {type : selectOptions[0].value , label : "" , data : ""}])
} else {
toast(intl.formatMessage({ id: 'MAX.ALLOWED.4' }), { type: "error" })
}
}
function deleteAction(index){
if(actions.length === 1 ){
toast(intl.formatMessage({ id: 'MIN.ALLOWED.1' }), { type: "error" })
} else {
const updatedFields = [...actions];
updatedFields.splice(index, 1);
console.log('index : ' , index)
console.log('updatedFields : ' , updatedFields)
setActions(updatedFields);
}
}
<div className='row my-6'>
<div className='col-lg-3 py-2'>
<h4><label className="form-label">{intl.formatMessage({ id: 'ACTIONS' })}*</label></h4>
<button className='btn btn-primary btn-sm btn-block' onClick={() => addAction()}>
<KTSVG path='/media/icons/duotune/arrows/arr075.svg' className='svg-icon-2' />
{intl.formatMessage({id: 'ADD.ACTION'})}
</button>
</div>
</div>
<div className='row my-6 '>
{ actions.map((item , index) => {
return(
<div key={index} className='row my-6'>
<div className='col-lg-4 py-2'>
<h4><label className="form-label">{intl.formatMessage({ id: 'TEMPLATE.TYPE' })}*</label></h4>
<Select
onChange={(value) => handleTypeChange(index, value)}
options={selectOptions}
/>
</div>
<div className='col-lg-3 py-2'>
<h4><label className="form-label">{intl.formatMessage({ id: 'TEMPLATE.LABEL' })}*</label></h4>
<input
{...formik_buttons_type.getFieldProps('action.label')}
className="form-control form-control-lg form-control-solid"
name='action.label'
id='action_label'
type="text"
maxLength={30}
onChange={(event) => handleLabelChange(index, event.target.value)}
value={actions.label}
required
onInvalid={(e) => checkLabelValidation(e)}
onInput={(e) => checkLabelValidation(e)}
/>
</div>
<div className='col-lg-3 py-2'>
<h4><label className="form-label">{intl.formatMessage({ id: 'TEMPLATE.DATA' })}*</label></h4>
<input
{...formik_buttons_type.getFieldProps('action.data')}
className="form-control form-control-lg form-control-solid"
name='action.data'
id='action_data'
type="text"
maxLength={100}
onChange={(event) => { handleDataChange(index, event.target.value); }}
value={actions.data}
required
onInvalid={(e) => checkDataValidation(e)}
onInput={(e) => checkDataValidation(e)}
/>
</div>
<div className='col-lg-2 py-2 mt-10'>
<OverlayTrigger
delay={{ hide: 50, show: 50 }}
overlay={(props) => (
<Tooltip {...props}>
{intl.formatMessage({ id: 'DEL.ACTION' })}
</Tooltip>
)}
placement="top">
<button
type='button'
style={{display: index === 0 ? 'none': 'inline-block'}}
className='btn btn-icon btn-md btn-bg-light btn-color-danger me-1'
onClick={() => deleteAction(index)}
>
<i className='fa fa-trash'></i>
</button>
</OverlayTrigger>
</div>
</div>
)
})}
</div>
I am able to receive exact index number perfect output from the logs below in deleteAction fields , but the view in browser deletes the last column(index) from the array of actions. :
console.log('index : ' , index)
console.log('updatedFields : ' , updatedFields)
can anyone help me with this ?
code sand box : https://codesandbox.io/s/vibrant-christian-bktnot
Thanks and Regards !
Upvotes: 0
Views: 401
Reputation: 142
Whenever using the index as a key for an element. We have to ensure we are not modifying the state array to avoid bugs. If you are modifying as @Dave suggested use unique keys.
The problem here is using the index as key, When we remove an element from an array react compares the previous keys [0,1,2,3]
with new keys [0,1,2]
.
If you notice closely, Even if we remove index (1) using splice(1,1)
method. The elements which are rendered again have starting index of 0.
React compares keys previous keys [0,1,2,3]
with new keys [0,1,2]
and finds out that index 3 is removed hence it every time removes the 3rd element in the above example (or the last index) from DOM. However, your state is reflecting the correct array element.
To avoid this use a unique key.
Codesandbox for a working example.
If you are not having keys in objects, To generate unique keys we can use one of the following as per your use case:
crypto.randomUUID();
Date.now().toString(36) + Math.random().toString(36).substr(2)
Upvotes: 1
Reputation: 64677
https://reactjs.org/docs/lists-and-keys.html
We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state.
When you do
{ actions.map((item , index) => {
return(
<div key={index}
you are pretty much asking for issues with component state. When react is trying to determine which element in the UI to update, and it sees "the array with a length of 6 now has a length of 5, which element do I need to remove", it will find the one with a key that no longer exists, in this case the one at the end since you used index as key, and will remove it.
I would probably do
function randomId(){
const uint32 = window.crypto.getRandomValues(new Uint32Array(1))[0];
return uint32.toString(16);
}
const [ actions , setActions ] = useState<any | undefined>([{id: randomId(), type : add_actions_options[0].value , label : "" , data : ""}])
function addAction(){
if(actions.length < 4 ){
setSelectOptions([...add_actions_options])
setActions([...actions , {id: randomId(), type : selectOptions[0].value , label : "" , data : ""}])
} else {
toast(intl.formatMessage({ id: 'MAX.ALLOWED.4' }), { type: "error" })
}
}
{ actions.map((item) => {
return(
<div key={item.id}
where randomId
can be anything that gives you a unique ID, or (even better) if there is an existing property on the actual data that uniquely identifies it, you could use that.
Upvotes: 0