Loop over array with map() function onClick (ReactJS) - arrays

I want to create a wizard that changes content when the user clicks a "next" button.
I'm currently trying to use the .map function, it works but how can I adjust my code to loop over each step in my array onClick?
Currently, my code just displays 3 separate inputs with all of the steps in the array, what I want to do is iterate over each step onClick.
Here is an example of my code:
Array:
const wizardControls = {
steps: [
{
step: 1,
name: 'name1',
type: 'text',
placeholder: 'name1',
},
{
step: 2,
name: 'name2',
type: 'text',
placeholder: 'name2',
},
{
step: 3,
name: 'name3',
type: 'text',
placeholder: 'name3',
},
],
};
JSX using map() function:
{steps.map((step, index) => (
<div key={index}>
<input
value={value}
name={step.name}
type={step.type}
placeholder={step.placeholder}
onChange={onChange}
/>
</div>
))}
I'm thinking the button will need a handler function to loop over the index, however, I'm unsure how to do this with the map() function.
I'm open to a better approach if the map() function isn't the best route.

One way you could do this is by slicing by which step you're on (based on index).
Here's an example of what that might look like with your code.
const [step, setStep] = useState(1)
...
steps.slice(step - 1, step).map((step, index) => (
...
))
See a working example here: https://codesandbox.io/s/pensive-northcutt-el9w6

If you want to show a step at a time, don't use Array.map() to render all of them. Use useState to hold the current index (step), and take the current item from the steps array by the index. To jump to the next step, increment the index by 1.
const { useState } = React;
const Demo = ({ steps }) => {
const [index, setIndex] = useState(0);
const [values, setValue] = useState([]);
const next = () =>
setIndex(step => step < steps.length -1 ? step + 1 : step);
const onChange = e => {
const val = e.target.value;
setValue(v => {
const state = [...v];
state[index] = val;
return state;
})
};
const step = steps[index];
return (
<div>
<input
value={values[index] || ''}
name={step.name}
type={step.type}
placeholder={step.placeholder}
onChange={onChange}
/>
<button onClick={next}>Next</button>
</div>
);
};
const wizardControls = {"steps":[{"step":1,"name":"name1","type":"text","placeholder":"name1"},{"step":2,"name":"name2","type":"text","placeholder":"name2"},{"step":3,"name":"name3","type":"text","placeholder":"name3"}]};
ReactDOM.render(
<Demo steps={wizardControls.steps} />,
root
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>

Related

Changing classname of a specific element on click from a mapped array an keeping the same classname until clicked again in react

const allTodos = [{id: 1, name: 'whatever user type'}, { }, { }, .... { }] // Defined as an array using setState in other file. Imported in this file as allTodos using props.
export const Todos = (props) => {
const [index, setIndex] = useState(0)
props.allTodos.map((prev) => {
return (
<div id="item_container">
<button type='button' className = `check_button ${prev.id === index ? 'checked' : 'not_checked'}`
onClick = {() => setIndex(prev.id)}>
<img src = {check} alt="" id = "check_img"></img>
</button>
<li id="li_item">{prev.name}</li>
</div>
)}
}
Explanation : Here, I set up a const index using useState that wiil change its value to the id of the element clicked upon in order to change the className of that element.
Question : Now, I succeeded in doing that but everytime I click on other element, the className gets added to that element but gets removed from the previous element it was added upon. Now I want to somehow preserve the className to those every elements I click on until I click on them again to change their className. By the way the styling that I desire to change is the background of that button/element.
You need to be able to keep track of every index that's checked - for this you'll need an array (or a number you do bit calculations with). A stateful index number doesn't contain enough information.
const allTodos = [{ id: 1, name: 'whatever user type' }, { id: 2, name: 'foo' }];
const Todos = ({ allTodos }) => {
const [todos, setTodos] = React.useState(() => allTodos.map(todo => ({ ...todo, checked: false })));
const handleClick = (i) => () => {
setTodos(todos.map((todo, j) => j !== i ? todo : ({ ...todo, checked: !todo.checked })))
};
return todos.map(({ id, name, checked }, i) => (
<div id="item_container">
<button
type='button'
className={`check_button ${checked ? 'checked' : 'not_checked'}`}
onClick={handleClick(i)}
>
<img alt="" id="check_img"></img>
</button>
<div>{name}</div>
</div >
))
}
ReactDOM.render(<Todos allTodos={allTodos} />, document.querySelector('.react'));
.checked {
background-color: green;
}
.not_checked {
background-color: yellow;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='react'></div>
You also need:
When not using plain ' " quotes, use {} delimiters around the prop
<li>s should only be children of <ul>s
Duplicate IDs are invalid HTML
You need to return the mapped JSX from the Todos component
You are missing a ) at the end of allTodos.map
You should use array to save ids clicked and removed id from array when clicked again.
const [idList, setIdList] = useState([])
const handleClick=(id, checked) => {
if(checked){
setIdList.filter(item => item !== id)
} else {
setIdList([...idList, id])
}
}
props.allTodos.map((prev) => {
const checked = idList.includes(prev.id)
...
className ={`check_button ${checked ? 'checked' : 'not_checked'}`}
onClick = {() => handleClick(prev.id, checked)
...

React Hooks Remove item from array

In my react hooks component I am rendering data from an array of objects.
const lineInfo = [
{
id: '001',
line: 's-1125026',
arrival: '7:30',
departure: '8:00',
authorization: 'User 1',
},
{
id: '002',
line: 's-1125027',
arrival: '8:01',
departure: '8:50',
authorization: 'User 1',
},
In the .map() I'm returning data using this data:
<div>
{lineInfo.map((line) => (
<Row key={`line_${line.id}`}>
// remaining code removed for length
The list returns fine, so now I am trying to remove a row from the list.
Remove func
const [list, setList] = useState(lineInfo);
function handleRemove(id) {
console.log('id' id);
const newList = list.filter((line) => line.id !== id);
setList(newList);
}
Remove Button
<Button
className={style.close_button}
onClick={() => handleRemove(line.id)}
>
<img src={close} alt="trash" />
</Button>
</Row>
The problem I am running into is that in my console log, is that only the line.id is being removed from the array instead of the whole row of data.
How do I remove all the data belonging to a particular id?
Even though the console log shows that the text is removed, why is the text that is displayed in my row not removed from the view?
I'm not too familiar with hooks and have only been able to find examples of my particular problem with class components. Thanks in advance.
You should display the defined state with the hook. in this case the list , and not the lineInfo.
<div>
{list.map((line) => (
<Row key={`line_${line.id}`}>
// remaining code removed for length
You should not render lineInfo, render the list from local state instead:
const { useState, useCallback } = React;
const lineInfo = [
{
id: '001',
line: 's-1125026',
arrival: '7:30',
departure: '8:00',
authorization: 'User 1',
},
{
id: '002',
line: 's-1125027',
arrival: '8:01',
departure: '8:50',
authorization: 'User 1',
},
];
//make row pure component using React.memo
const Row = React.memo(function Row({ item, remove }) {
return (
<div>
<pre>{JSON.stringify(item, undefined, 2)}</pre>
<button onClick={() => remove(item.id)}>
remove
</button>
</div>
);
});
const App = () => {
const [list, setList] = useState(lineInfo);
//make remove a callback that is only created
// on App mount using useCallback with no dependencies
const remove = useCallback(
(removeId) =>
//pass callback to setList so list is not a dependency
// of this callback
setList((list) =>
list.filter(({ id }) => id !== removeId)
),
[]
);
return (
<ul>
{list.map((line) => (
<Row
key={`line_${line.id}`}
item={line}
remove={remove}
/>
))}
</ul>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

How to re-render react component when mapping over state that is array of objects

I am trying to map over an array of objects in state, conditionally returning one of two react components from that state. I then change that state at some point and would expect the component to re-render when it's object's state changed. I understand my issue is something to do with React not recognizing the change in the diff, but I'm not sure why and what pattern I need to change to in order to get this working.
Here's a codepen:
https://codepen.io/steven-harlow/pen/KKPLXRO
And the code from it:
const App = (props) => {
const [todos, setTodos] = React.useState([
{name: 'A', done: false},
{name: 'B', done: false},
{name: 'C', done: false},
])
React.useEffect(() => {
}, [todos])
const handleClick = (name) => {
const index = todos.find(todo => todo.name == name)
let tempTodos = todos;
tempTodos[index].done = true;
setTodos(tempTodos);
}
return (
<div>
<h1>Hello, world!</h1>
<div>
{todos.map(todo => {
return todo.done ? (<div key={'done' + todo.name}>{todo.name} : done</div>) : (<div onClick={() => handleClick(todo.name)} key={'notdone' + todo.name}>{todo.name} : not done</div>)
})}
</div>
</div>
)
}
Here you go, this here should work for you now. I added some notes in there.
const App = (props) => {
const [todos, setTodos] = React.useState([
{name: 'A', done: false},
{name: 'B', done: false},
{name: 'C', done: false},
])
const handleClick = (name) => {
/*
Here you were using todos.find which was returning the object. I switched
over to todos.findIndex to give you the index in the todos array.
*/
const index = todos.findIndex(todo => todo.name === name)
/*
In your code you are just setting tempTodos equal to todos. This isn't
making a copy of the original array but rather a reference. In order to create
a copy I am adding the .slice() at the end. This will create a copy.
This one used to get me all of the time.
*/
let tempTodos = todos.slice();
tempTodos[index].done = true;
setTodos(tempTodos);
}
console.log(todos)
return (
<div>
<h1>Hello, world!</h1>
<div>
{todos.map((todo,index) => {
return todo.done ? (<div key={index}>{todo.name} : done</div>) : (<div onClick={() => handleClick(todo.name)} key={index}>{todo.name} : not done</div>)
})}
</div>
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
Another thing I did was simplify the keys for the divs created by the map. I just added the index to the map and used that for the key, a lot cleaner that way.
Hope this helps!
React won't see the state as changed unless you create a new array.
const handleClick = n => setTodos(todos.map(t => t.name === n ? {...t, done: true} : t));

Removing object from array using hooks (useState)

I have an array of objects. I need to add a function to remove an object from my array without using the "this" keyword.
I tried using updateList(list.slice(list.indexOf(e.target.name, 1))). This removes everything but the last item in the array and I'm not certain why.
const defaultList = [
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }]
const [list, updateList] = useState(defaultList);
const handleRemoveItem = e => {
updateList(list.slice(list.indexOf(e.target.name, 1)))
}
return (
{list.map(item => {
return (
<>
<span onClick={handleRemoveItem}>x </span>
<span>{item.name}</span>
</>
)}
}
)
Expected behaviour: The clicked item will be removed from the list.
Actual behaviour: The entire list gets removed, minus the last item in the array.
First of all, the span element with the click event needs to have a name property otherwise, there will be no name to find within the e.target. With that said, e.target.name is reserved for form elements (input, select, etc). So to actually tap into the name property you'll have to use e.target.getAttribute("name")
Additionally, because you have an array of objects, it would not be effective to use list.indexOf(e.target.name) since that is looking for a string when you are iterating over objects. That's like saying find "dog" within [{}, {}, {}]
Lastly, array.slice() returns a new array starting with the item at the index you passed to it. So if you clicked the last-item, you would only be getting back the last item.
Try something like this instead using .filter(): codesandbox
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const App = () => {
const defaultList = [
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }
];
const [list, updateList] = useState(defaultList);
const handleRemoveItem = (e) => {
const name = e.target.getAttribute("name")
updateList(list.filter(item => item.name !== name));
};
return (
<div>
{list.map(item => {
return (
<>
<span name={item.name} onClick={handleRemoveItem}>
x
</span>
<span>{item.name}</span>
</>
);
})}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You can use Array.filter to do this in a one-liner:
const handleRemoveItem = name => {
updateList(list.filter(item => item.name !== name))
}
Eta: you'll also need to pass the name of your item in your onClick handler:
{list.map(item => {
return (
<>
<span onClick={() =>handleRemoveItem(item.name)}>x </span>
<span>{item.name}</span>
</>
)}
const defaultList = [
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }
]
const [list, updateList] = useState(defaultList);
const handleRemoveItem = idx => {
// assigning the list to temp variable
const temp = [...list];
// removing the element using splice
temp.splice(idx, 1);
// updating the list
updateList(temp);
}
return (
{list.map((item, idx) => (
<div key={idx}>
<button onClick={() => handleRemoveItem(idx)}>x </button>
<span>{item.name}</span>
</div>
))}
)
Small improvement in my opinion to the best answer so far
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const App = () => {
const defaultList = [
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }
];
const [list, updateList] = useState(defaultList);
const handleRemoveItem = (item) => {
updateList(list.filter(item => item.name !== name));
};
return (
<div>
{list.map(item => {
return (
<>
<span onClick={()=>{handleRemoveItem(item)}}>
x
</span>
<span>{item.name}</span>
</>
);
})}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Instead of giving a name attribute we just send it to the handle function
I think this code will do
let targetIndex = list.findIndex((each) => {each.name == e.target.name});
list.splice(targetIndex-1, 1);
We need to check name value inside object so use findIndex instead. then cut the object start from target index to 1 array after target index.
Codepen
From your comment your problem came from another part.
Change this view section
return (
<>
<span onClick={() => handleRemoveItem(item) }>x </span>
<span>{item.name}</span>
</>
)}
change function handleRemoveItem format
const handleRemoveItem = item => {
list.splice(list.indexOf(item)-1, 1)
updateList(list);
}
Redundant one liner - would not recommend as hard to test / type / expand / repeat / reason with
<button onClick={() => setList(list.slice(item.id - 1))}
A version without exports:
const handleDeleteItem = id => {
const remainingItems = list.slice(id - 1)
setList(remainingItems);
}
However I would consider expanding the structure of your logic differently by using helper functions in another file.
With that in mind, I made one example for filter and another for slice. I personally like the slice option in this particular use-case as it makes it easy to reason with. Apparently, it is also slightly more performant on larger lists if scaling (see references).
If using slice, always use slice not splice unless you have good reason not to do so as it adheres to a functional style (pure functions with no side effects)
// use slice instead of splice (slice creates a shallow copy, i.e., 'mutates' )
export const excludeItemFromArray = (idx, array) => array.slice(idx-1)
// alternatively, you could use filter (also a shallow copy)
export const filterItemFromArray = (idx, array) => array.filter(item => item.idx !== idx)
Example (with both options filter and slice options as imports)
import {excludeItemFromArray, filterItemFromArray} from 'utils/arrayHelpers.js'
const exampleList = [
{ id: 1, name: "ItemOne" },
{ id: 2, name: "ItemTwo" },
{ id: 3, name: "ItemThree" }
]
const [list, setList] = useState(exampleList);
const handleDeleteItem = id => {
//excluding the item (returning mutated list with excluded item)
const remainingItems = excludeItemFromArray(id, list)
//alternatively, filter item (returning mutated list with filtered out item)
const remainingItems = filterItemFromArray(id, list)
// updating the list state
setList(remainingItems);
}
return (
{list.map((item) => (
<div key={item.id}>
<button onClick={() => handleDeleteItem(item.id)}>x</button>
<span>{item.name}</span>
</div>
))}
)
References:
Don't use index keys in maps: https://robinpokorny.com/blog/index-as-a-key-is-an-anti-pattern/
Performance of slice vs filter: https://medium.com/#justintulk/javascript-performance-array-slice-vs-array-filter-4573d726aacb
Slice documentation: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
Functional programming style: https://blog.logrocket.com/fundamentals-functional-programming-react/#:~:text=Functional%20programming%20codes%20are%20meant,computations%20are%20called%20side%20effects.
Using this pattern, the array does not jump, but we take the previous data and create new data and return it.
const [list, updateList] = useState([
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }
]);
updateList((prev) => {
return [
...prev.filter((item, i) => item.name !== 'ItemTwo')
]
})
This is because both slice and splice return an array containing the removed elements.
You need to apply a splice to the array, and then update the state using the method provided by the hook
const handleRemoveItem = e => {
const newArr = [...list];
newArr.splice(newArr.findIndex(item => item.name === e.target.name), 1)
updateList(newArr)
}

List Updates Incorrectly

A very simple list with inputs that are supposed to be addable and updatable.
The problem occurs after I add one or more inputs and then try to type inside of one of the inputs - all inputs after the one being typed in disappear.
It has something to do with memo-ing the Item component and I'm looking to understand what exactly is happening there (valueChanged getting cached?). I can't wrap my head around.
Without the memo function the code works as expected but of course, all inputs get updated on every change.
Here's a gif of what's happening: https://streamable.com/gsgvi
To replicate paste the code below into an HTML file or take a look here: https://codesandbox.io/s/81y3wnl142
<style>
ul {
list-style-type:none;
padding:0;
}
input[type=text] {
margin:0 10px;
}
</style>
<div id="app"></div>
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/babel-standalone#6.15.0/babel.min.js"></script>
<script type="text/babel">
const randomStr = () => Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10);
const { useState, memo, Fragment } = React
const Item = memo(({ value, i, valueChanged }) => {
console.log('item rendering...');
return <li>
<input type='text' value={value} onChange={e => valueChanged(i, e.target.value)}/>
</li>
}, (o, n) => o.value === n.value)
const ItemList = ({ items, valueChanged }) => {
return <ul>{
items.map(({ key, title }, i) => (
<Item
key={key}
i={i}
value={title}
valueChanged={valueChanged}
/>
))
}</ul>
}
const App = () => {
const [items, setItems] = useState([
{ key: randomStr(), title: 'abc' },
{ key: randomStr(), title: 'def' },
])
const valueChanged = (i, newVal) => {
let updatedItems = [...items]
updatedItems[i].title = newVal
setItems(updatedItems)
}
const addNew = () => {
let newItems = [...items]
newItems.push({ key: randomStr(), title:'' })
setItems(newItems)
}
return <Fragment>
<ItemList items={items} valueChanged={valueChanged}/>
<button onClick={addNew}>Add new</button>
</Fragment>
}
ReactDOM.render(<App/>, document.querySelector('#app'))
</script>
valueChanged is a closure which gets a fresh items every render. But your memo doesn't trigger an update because it only checks (o, n) => o.value === n.value. The event handler inside Item uses the old items value.
It can be fixed with functional updates:
const valueChanged = (i, newVal) => {
setItems(oldItems => {
let updatedItems = [...oldItems];
updatedItems[i].title = newVal;
return updatedItems;
});
}
So valueChanged doesn't depend on items and doesn't need to be checked by memo.
Your code might have similar problems with other handlers. It is better to use functional updates whenener new value is based on the old one.
You can try initializing the state this way, I tried it on codesandbox and it may be the behavior you are looking for.
https://codesandbox.io/s/q8l6m2lj64
const [items, setItems] = useState(() => [
{ key: randomStr(), title: 'abc' },
{ key: randomStr(), title: 'def' },
]);

Resources