So i want to build a voting app thing, and I need to set the number of votes into state so that it updates on click.But I don't exactly know how to set one property of an object without disrupting the other oroperty and the other object.
import './App.css';
import React, {useState} from 'react'
function App() {
const [votes, setVotes] = useState([
{
voteNum: 0,
name: "Item 1"
},
{
voteNum: 0,
name: "Item 2"
}
])
const addVote = (vote) => {
vote.voteNum++
setVotes( /* How do I set the votes.voteNum to be equal to vote.voteNum from the line above */)
}
return (
<div className="App">
<h1>Cast Your Votes!</h1>
{votes.map(vote => (
<form className="votee" onClick={() => addVote(vote)}>
<h1>{vote.voteNum}</h1>
<h2>{vote.name}</h2>
</form>
))}
</div>
);
}
export default App;
You should update react state in immutable way:
const addVote = (vote) => {
setVotes(votes.map(x => x === vote ? ({
...x,
voteNum: x.voteNum + 1
}) : x))
}
Or better use some unique Id in comparison, e.g. x.id === vote.id if you have Ids for your objects; or as mentioned in another answer, you can also use element array index in comparison (pass that to the function instead of vote).
const addVote = (vote) => {
const _votes = JSON.parse(JSON.stringify(votes))
const index = _votes.findIndex(v => v.name === vote.name)
if (index !== -1) {
_votes[index].voteNum ++
}
setVotes(_votes)
}
Pass index to function
{votes.map((vote, index) => (
<form className="votee" onClick={() => addVote(index)}>
<h1>{vote.voteNum}</h1>
<h2>{vote.name}</h2>
</form>
))}
addVote function would look like
const addVote = (index) => {
votes[index].voteNum ++;
setVotes([...votes]);
}
Note: Usestate is prefrered to use primitive data types i.e string, boolean and number
For array and object make use of useReducer
Related
I have seen similar questions but none of the answers are working in my case and I hope someone will be able to tell me why.
My ReactApp renders 3 card components that flip when clicked on. The cards are populated with data from an object array and is rendered with a map function (added info in case it has an impact). Here is parent component.
import React from 'react'
import FlipCard from './FlipCard'
const cards = [
{
id: 1,
text: 'NOPE',
},
{
id: 2,
text: '!!WINNER!!',
},
{
id: 3,
text: 'NOPE',
},
]
const shuffleCards = array => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const temp = array[i]
array[i] = array[j]
array[j] = temp
}
return array
}
shuffleCards(cards)
console.log(cards)
const CardGameUI = () => {
shuffleCards(cards)
return (
<div className="cards-ui">
{cards.map(card => (
<FlipCard key={card.id} text={card.text} value={card.id} />
))}
</div>
)
}
export default CardGameUI
When one of the cards are flipped, I need the onClick for the other cards to be disabled. I tried using state and a conditional in my onClick event but it has no effect. The according to the console.log, the state of the play boolean is changed and if I manually change the conditional in the onCLick event check if play is true, then it works perfectly fine. I am clearly missing something because it seems as though the conditional is working and the state is changing.
import React, { useState } from 'react'
import ReactCardFlip from 'react-card-flip'
import FrontComponent from './FrontComponent'
import BackComponent from './BackComponent'
const FlipCard = ({ text, value }) => {
const [isFlipped, setIsFlipped] = useState(false)
const [activeCard, setActiveCard] = useState(2)
const [play, setPlay] = useState(false)
console.log(play.valueOf())
function handleFlip() {
setPlay(true)
setIsFlipped(!isFlipped)
console.log(isFlipped)
setActiveCard(value)
console.log(value)
}
if (activeCard !== 2) {
console.log('Play again?')
}
return (
<>
<ReactCardFlip isFlipped={isFlipped} flipDirection="horizontal">
<FrontComponent onClick={!play ? handleFlip : null} />
<BackComponent text={text} value={value} />
</ReactCardFlip>
</>
)
}
export default FlipCard
What am I missing?
You should manage the onClick event and the flip state on the parent instead of inside the card.
An high-level overview will be:
const CardGameUI = () => {
const [flipped, setFlipped] = useState({});
const hasFlipped = Object.values(flipped).includes(true);
const handleFlip = id => () => setFlipped(flipped => ({
...flipped,
[id]: true // can be changed to toggle in case you need it in the future
}));
return (
<div>
{cards.map(({ id, text }) => (
<FlipCard
key={id}
text={text}
value={id}
onFlip={handleFlip(id)}
flipped={flipped[id]}
disabled={hasFlipped}
/>
))}
</div>
)
};
const FlipCard = ({ text, value, onFlip, flipped , disabled}) => {
return (
<ReactCardFlip isFlipped={flipped} flipDirection="horizontal">
<FrontComponent onClick={onFlip} disabled={disabled} />
<BackComponent text={text} value={value} />
</ReactCardFlip>
)
}
I have the following code, which the purpose it is to be reused inside another component, and give a different link according to the id of my object, and my API return.
The problem is that, how I'm using a map function it is reproducing the same button multiple times on the screen. I want to know how I get only one button. Can I somehow use indexOf inside that map?
Everything else is working just fine
const ButtonGroup = (linksReturn) => {
return linksReturn.map((urlAdress, index) => (
<div key={index}>
<button
target="_blank"
href={urlAdress["detailsUrl"]}
>
Acess Link
</button>
</div>
));
};
const PreviewActions = ({ linksReturn }) => {
return <div>{ButtonGroup(linksReturn)}</div>;
};
export default PreviewActions;
This is the part of the code where I reuse the button
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import PreviewActions from './PreviewActions';
// ** Store & Actions
import { getInfo } from '../store/actions/index';
import { useDispatch, useSelector } from 'react-redux';
const LicitacaoPreview = () => {
const dispatch = useDispatch();
const store = useSelector((state) => state.allInfo);
const { id } = useParams();
useEffect(() => {
dispatch(getInfo(id));
}, [dispatch]);
return typeof store.lotesData === 'object' ? (
<div>
<PreviewActions
id={id}
linksReturn={store.infoData}
/>
</div>
)
};
export default LicitacaoPreview;
Array.prototype.map() does not work like that, it calls the function passed in as an argument on every single array elements and returns the resultant array.
A better approach would be using Array.prototype.slice() to return specific part of the original array and then use map() on it
const ButtonGroup = (linksReturn) => {
const newArr = linksReturn.slice(startindex, endindex); // replace with your index appropriately
return newArr.map((urlAdress, index) => (
<div key={index}>
<button
target="_blank"
href={urlAdress["detailsUrl"]}
>
Acess Link
</button>
</div>
));
};
I have an array of objects called data. I loop this array and render the Counter component. Increment and decrement of the counter value are passed as props to the component.
But if I change the value in a one-component, the other two components also re-renders. Which is not needed. How do I prevent this behavior? I tried memo and useCallback but seems not implemented correctly.
Counter.js
import React, { useEffect } from "react";
const Counter = ({ value, onDecrement, onIncrement, id }) => {
useEffect(() => {
console.log("Function updated!", id);
}, [onDecrement, onIncrement]);
return (
<div>
{value}
<button onClick={() => onDecrement(id)}>-</button>
<button onClick={() => onIncrement(id)}>+</button>
</div>
);
};
export default React.memo(Counter);
Home.js
import React, { useState, useCallback } from "react";
import Counter from "../components/Counter";
export default function Home() {
const [data, setData] = useState([
{
id: 1,
value: 0,
},
{
id: 2,
value: 0,
},
{
id: 3,
value: 0,
},
]);
const onIncrement = useCallback(
(id) => {
setData((e) =>
e.map((record) => {
if (record.id === id) {
record.value += 1;
}
return record;
})
);
},
[data]
);
const onDecrement = useCallback(
(id) => {
setData((e) =>
e.map((record) => {
if (record.id === id) {
record.value -= 1;
}
return record;
})
);
},
[data]
);
return (
<div>
<h1>Home</h1>
{data.map((e) => {
return (
<Counter
value={e.value}
onDecrement={onDecrement}
onIncrement={onIncrement}
id={e.id}
/>
);
})}
</div>
);
}
I suspect useCallback & useMemo are not helpful in this case, since you're running an inline function in your render:
{data.map(e => <Counter ...>)}
This function will always returns a fresh array & the component will always be different than the previous one.
In order to fix this, I think you'd want to memoize that render function, not the Counter component.
Here's a simple memoized render function with useRef:
// inside of a React component
const cacheRef = useRef({})
const renderCounters = (data) => {
let results = []
data.forEach(e => {
const key = `${e.id}-${e.value}`
const component = cacheRef.current[key] || <Counter
value={e.value}
key={e.id}
onDecrement={onDecrement}
onIncrement={onIncrement}
id={e.id}
/>
results.push(component)
cacheRef.current[key] = component
})
return results
}
return (
<div>
<h1>Home</h1>
{renderCounters(data)}
</div>
);
In the codesandbox below, only the clicked component log its id:
https://codesandbox.io/s/vibrant-wildflower-0djo4?file=/src/App.js
Disclaimer: With this implementation, the component will only rerender if its data value changes. Other props (such as the increment/decrement callbacks) will not trigger changes. There's also no way to clear the cache.
Memoize is also trading memory for performance — sometimes it's not worth it. If there could be thousands of Counter, there're better optimiztion i.e. changing UI design, virtualizing the list, etc.
I'm sure there's a way to do this with useMemo/React.memo but I'm not familiar with it
I'm learning react. I am trying to sort a list based on name. The ShoppingList component is
const ShoppingList = () => {
const [items, setItems] = useState([]);
const data = [
{id: 1, name: 'Soda'},
{id: 2, name: 'ice'},
];
useEffect(() => {
setItems(data);
}, []);
const handleSort = () => {}
return ();
}
On a button click I'm trying to sort the data and display it.
<button onClick={() => handleSort()}>Sort by name</button>
Inside the handleSort() function
const sortItems = items.sort((a, b) => {
const nameA = a.name.toUpperCase();
const nameB = b.name.toUpperCase();
if(nameA < nameB)
return -1;
if(nameA > nameB)
return 1;
return 0;
});
console.log(sortItems);
setItems(sortItems);
The console.log(sortItems) shows the sorted array. But not rendering in the DOM.
Inside the return, I'm trying to display the sorted data in this format
<ul>
{items.map((item) => {
return (
<li key={item.id}>
<span>{item.name} </span>
<button onClick={() => handleRemove(item.id)}>×</button>
</li>
);
})
}
</ul>
What i'm missing here?
I'd recommend deriving the sorted list of items with useMemo, so it's "derived state" dependent on the items array and the desired sort order.
Don't use useEffect for initial state. useState accepts a creator function for the initial state instead.
localeCompare is a neater way to return -1, 0, +1 for comparison.
[...items] (a shallow copy of items) is required, because .sort() sorts an array in-place.
const sortByName = (a, b) => a.name.toUpperCase().localeCompare(b.name.toUpperCase());
const ShoppingList = () => {
const [items, setItems] = useState(() => [
{ id: 1, name: "Soda" },
{ id: 2, name: "ice" },
]);
const [sortOrder, setSortOrder] = useState("original");
const sortedItems = React.useMemo(() => {
switch (sortOrder) {
case "byName":
return [...items].sort(sortByName);
default:
return items;
}
}, [items, sortOrder]);
return (
<>
<button onClick={() => setSortOrder("byName")}>Sort by name</button>
<button onClick={() => setSortOrder("original")}>Sort in original order</button>
<ul>
{sortedItems.map((el, i) => (
<li key={el.id}>
<span>{el.name} </span>
<button>×</button>
</li>
))}
</ul>
</>
);
};
First of all you need to stop using useEffect for the initial state,
And if you want react to notice your changes, use an object instead of array. (This is not always that react doesn't notice your changes, but since you didn't change array and it was only sorted, react ignores it).
const ShoppingList = () => {
const [items, setItems] = useState({
data: [
{ id: 1, name: 'Soda' },
{ id: 2, name: 'ice' },
],
});
const handleSort = () => {
const sortedItems = items.data.sort((a, b) => {
const nameA = a.name.toUpperCase();
const nameB = b.name.toUpperCase();
if (nameA < nameB) return -1;
if (nameA > nameB) return 1;
return 0;
});
setItems({
data: sortedItems,
});
};
return (
<>
<button onClick={() => handleSort()}>Sort by name</button>
<ul>
{items.data.map((el, i) => (
<li key={el.id}>
<span>{el.name} </span>
<button onClick={() => handleRemove(item.id)}>×</button>
</li>
))}
</ul>
</>
);
}
Hope this helps 🙂
If you are interested to know more indepth on why the array items is changed (sorted) but React doesn't render, there are 2 things to take note:
How array.sort work
How React re-render with useState
For (1), it's easy, array.sort return the sorted array. Note that the array is sorted in place, and no copy is made. Hence sortItems and items still refer to the same array
For (2), it's a little bit complicated as we have to read through React code base.
This is React.useState signature
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
Use Github navigation tools and scan we gonna end-up to this code:
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
mountHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
}
Next we must find the definition of mounState:
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
...
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
Notice, the return type of mountState is an array where the 2nd argument is an function just like const [items, setItems] = useState([])
Which means we almost there.
dispatch is the value from dispatchAction.bind
Scan through the code we gonna end up at this line:
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
Last part is what function is does:
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
It simply check for equality using === operator.
Comeback to your sort function, nextState in our case is sortItems and prevState is items. With (1) in mind, sortItems === items => true so React gonna skip the rendering.
That's why you see most of the tutorials states that you have to do shallow copy.
By doing so your nextState will differ from your prevState.
TLDR:
React use function is above to check for state changes when using hooks
Always make shallow copy when working with array, object if you are using hooks
I'm having a following situation where I want to know the indexOf value to be able to use that knowledge later on in the code. How ever, I've tried multiple different ways to get the value and I don't seem to get it right. In the code below I've tried a few different ways that I found searching Stackoverflow. All of them return -1 so far, meaning that there is either something wrong with my code or some other issue I'm not able to find at the moment.
FYI, selectedGroup is an array with objects inside, just like this:
[{label: "somelabel", value: 100}]
and there can be many of them, depends on the user.
code:
import React, { useState, useEffect } from 'react';
const GroupButtonMaker = ({ selectedGroup}) => {
const [newButtons, setNewButtons] = useState([]);
console.log(newButtons);
useEffect(() => {
const createButtons = () => {
setNewButtons(
selectedGroup.map(item => {
return (
<button
id={item.value}
className={'btn green micro'}
key={item.value}
onClick={event => btnHandler(event)}
>
{item.label}
</button>
);
})
);
};
createButtons();
}, [selectedGroup]);
const btnHandler = event => {
//here at the handler I'm trying multiple different ways to find indexOf as a test. No luck so far.
const eventID = event.currentTarget.id;
let currentTargetIndex = newButtons
.map(item => item.value)
.indexOf(eventID);
console.log(currentTargetIndex);
console.log(newButtons.findIndex(x => x.value === eventID));
};
Array.prototype.indexOfObject = function arrayObjectIndexOf(property, value) {
for (var i = 0, len = this.length; i < len; i++) {
if (this[i][property] === value) return i;
}
return -1;
};
// here i've hardcored one value as a test to see if it works but it didn't so far.
console.log(newButtons.indexOfObject('value', 107));
const idx = newButtons.reduce(function(cur, val, index, eventID) {
if (val.value === eventID && cur === -1) {
return index;
}
return cur;
}, -1);
console.log(idx);
return <ul>{newButtons.map(item => item)}</ul>;
};
export default GroupButtonMaker;
Thank you beforehand for any suggestions to my current problem. Hopefully I've managed to describe the problem in a way that makes it solveable. If not, please ask and I'll try to provide an answer.
Why not simply pass the id of the button to the handler instead of getting it from event.
You can achieve it by this: onClick={(event) => btnHandler(item.value)}
And then in your btnHandler, just look up the index of the selected button from selectedGroup instead of newButtons.
Here, give this a try:
import React, { useState, useEffect } from "react";
const GroupButtonMaker = ({ selectedGroup }) => {
const [newButtons, setNewButtons] = useState([]);
useEffect(() => {
const buttons = selectedGroup.map(item => {
return (
<button
id={item.value}
className={"btn green micro"}
key={item.value}
onClick={(event) => btnHandler(item.value)}
>
{item.label}
</button>
);
});
setNewButtons(buttons);
}, [selectedGroup]);
const btnHandler = buttonId => {
const selectedButtonIndex = selectedGroup.findIndex(item => item.value === buttonId);
console.log("selectedButtonIndex is: ", selectedButtonIndex);
};
return <ul>{newButtons.map(item => item)}</ul>;
};
export default GroupButtonMaker;
Here's a Working Sample Code Demo for your ref.