How can I store the accumulated value(integer) in React? - javascript

I have an array of anecdotes and two buttons. One is to vote and the other is to randomly select the next anecdotes. When I click the vote button, the vote is seen as increasing numbers. However, the problem is that when I click on the next anecdotes, the vote remains instead of resetting back to the number zero for new anecdote.
I tried to store the vote numbers by first adding a new attribute Vote with an empty value "" to the array anecdotes, anecdotes.forEach(function (anecdote) { anecdote.Vote = "" }); and then pushing the increasing vote numbers to a new copy of an array const copyAnecdotes = [...anecdotes] but it didn't work. How can I make this work ?
App.js
import { useState } from 'react'
const inlineParagraph = {
display: 'inline-flex',
flexDirection: 'column',
}
const inlineButton = {
display: 'flex',
marginTop: '5px',
gap: '10px',
}
const Button = (props) => {
return (
<div>
<button onClick={props.handleClick}> {props.text} </button>
</div>
)
}
const App = () => {
const handleClick2 = () => {
setSelected(Math.floor(Math.random()*anecdotes.length))
}
const handleVote = () => {
setVote(vote + 1)
}
const anecdotes = [
'If it hurts, do it more often.',
'Adding manpower to a late software project makes it later!',
'The first 90 percent of the code accounts for the first 10 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.',
'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
'Premature optimization is the root of all evil.',
'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.',
'Programming without an extremely heavy use of console.log is same as if a doctor would refuse to use x-rays or blood tests when diagnosing patients.',
'The only way to go fast, is to go well.'
]
const [selected, setSelected] = useState(0)
const [vote, setVote] = useState(0)
const copyAnecdotes = [...anecdotes]
return (
<div style={inlineParagraph}>
{ anecdotes[selected] }
<div style={inlineButton} >
<Button handleClick={handleClick2} text="next anecdotes" setSelected={setSelected} />
<Button handleClick={handleVote} text="vote" setSelected={setSelected} /> {vote}
</div>
</div>
)
}
export default App

First you need an array of votes:
const [votes, setVotes] = useState(() => Array(anecdotes.length).fill(0));
Then a function to add votes:
const handleVotes = () => {
setVote((votes) =>
votes.map((vote, index) => (index === selected ? vote + 1 : vote))
);
};
And display correct votes:
{votes[selected]}

You should reset the vote inside your handleClick2 function
const handleClick2 = () => {
setSelected(Math.floor(Math.random()*anecdotes.length));
setVote(0);
}

Related

React Stepper with dynamic and responsive steps

I'm building a React stepper with MUI and I wanted to have it be dynamic depending on the data coming in and also to be able to add/remove steps within that dynamic section. I was not able to find any examples or posts on here regarding what I had in mind, and the docs for MUI's stepper don't go anywhere near touching dynamic/responsive steps like this. I've been able to get it pretty far (actually a bit surprised I got it as far as I have), but I'm stuck right at the end. I've set up a generic example on CSB (link below), with the stepper pulling in data objects, displaying them on the dynamic steps, and the add/remove functionality works for the step labels, but not for the content. This is where I can't figure it out, I set up the structure the same for the labels and content, and since is basically just pushing to/filtering out arrays, I'm not finding where the difference is. I figure I'd post it here before messing with it any further. Also, if anyone has any suggestions on how to clean it up, I'd be happy to hear those as well, I'm sure it is a bit messy and heavy handed in getting the job done.
Heres the main Stepper component:
const DynamicStepper = () => {
const [open, setOpen] = useState(false);
const [activeStep, setActiveStep] = useState(0);
const [copiedObjs, setCopiedObjs] = useState([]);
const [copiedLabels, setCopiedLabels] = useState([]);
const [middleContent, setMiddleContent] = useState([]);
const [stepsContent, setStepsContent] = useState([])
//Dialog actions
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
//Stepper actions
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
}
//Setting up dynamic labels (actual code involves conditional api calls)
useEffect(() => {
if(data) {
setCopiedObjs(data)
}
}, [])
useEffect(() => {
if(copiedObjs.length) {
let labeling = copiedObjs.map((i) => (
i.title
))
setCopiedLabels(labeling)
}
}, [copiedObjs])
//Set labels for stepper
const initialLabels = ["First", "Second", "Third"]
const finalLabels = ["One from the End", "Final"]
const steps = initialLabels.concat(copiedLabels).concat(finalLabels)
//Set content for stepper
//moved into useEffect
// function getMiddleContent(){
// const content = copiedObjs.map((obj, idx) => (
// <StepDynamic key={idx} props={obj} />
// ))
// setMiddleContent(content)
// }
useEffect(() => {
const content = copiedObjs.map((obj, idx) => (
<StepDynamic key={idx} props={obj} />
))
setMiddleContent(content)
}, [middleContent, copiedObjs])
useEffect(() => {
//add/delete steps
function addStep(n = 1){
let newSteps = [...copiedLabels];
let newContent = [...middleContent];
let newLabel = ["new obj"]
newSteps.push(...newLabel);
console.log("midContent pre push: ", middleContent)
let content = [<StepDynamic key={Math.random()*3} props={null} />]
newContent.push(...content)
console.log("postPush, newContent: ", content)
setCopiedLabels(newSteps)
setMiddleContent(content)
}
function removeStep(idx){
let newSteps = [...copiedLabels];
let newContent = [...middleContent];
let steps = newSteps.filter((item, i) => i !== idx);
let content = newContent.filter((item, i) => i !== idx)
setCopiedLabels(steps)
setMiddleContent(content)
}
const initialContent = [<StepOne key={1} />, <StepTwo key={2} />, <StepThree key={3} addStep={addStep} removeStep={removeStep} titles={copiedLabels} />]
const finalContent = [<StepPenUltimate key={4} />, <StepFinal key={5} />]
const content = initialContent.concat(middleContent).concat(finalContent)
setStepsContent(content)
}, [middleContent, copiedLabels])
function getStepsContent(stepIndex) {
return stepsContent[stepIndex]
}
//Moved this section into useEffect to see if I got better results, but responds the same as before
// //add/delete steps
// function addStep(n = 1){
// let newSteps = [...copiedLabels];
// let newContent = [...middleContent];
// let newLabel = ["new obj"]
// newSteps.push(...newLabel);
// console.log("midContent pre push: ", middleContent)
// let content = [<StepDynamic key={Math.random()*3} props={null} />]
// newContent.push(...content)
// console.log("postPush, newContent: ", content)
// setCopiedLabels(newSteps)
// setMiddleContent(content)
// }
// function removeStep(idx){
// let newSteps = [...copiedLabels];
// let newContent = [...middleContent];
// let steps = newSteps.filter((item, i) => i !== idx);
// let content = newContent.filter((item, i) => i !== idx)
// setCopiedLabels(steps)
// setMiddleContent(content)
// }
return (
<>
<Button variant="contained" color="primary" onClick={handleClickOpen}>
New Stepper
</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Stepper Guide</DialogTitle>
<DialogContent>
<DialogContentText>
Just some words of guidance
</DialogContentText>
<div>
<Stepper activeStep={activeStep} alternativeLabel>
{steps && steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
<br />
</Stepper>
<>
{activeStep === steps.length ? (
"Complete"
) : (
<>
{getStepsContent(activeStep)}
<Button color='warning' onClick={handleBack}>
{activeStep === steps[0] ? "" : "Back"}
</Button>
<Button color="primary" onClick={handleNext}>
{activeStep === steps.length ? "Submit" : "Next"}
</Button>
</>
)}
</>
</div>
<DialogActions>
<Button color="error" onClick={handleClose}>
Cancel
</Button>
</DialogActions>
</DialogContent>
</Dialog>
</>
)
}
And heres the CSB: https://codesandbox.io/s/eager-cerf-tsrbci?file=/src/Stepper.js:463-5442
UPDATE
After playing around and a lot of console.logs I think I've figured out where the issue is, but I'm not sure how to resolve it. When adding/ removing a step, the functions are wrapped within the same useEffect as the logic to set the content for the steps. This was done to be able to provide the functions to the StepThree component where the adding/ removing takes place on the UI. However, it seems the state update and the logic setting the content are not on the same time cycle and so the content setting does not have access to the new updated state. I've tried breaking apart the useEffect into multiple and tried just writing separate functions, but because there is a lot of conditional state being utilized, and in order for the different pieces to correctly have access, as well as to avoid infinite re-render loops, I keep coming back to the same setup. The label setting works fine because the setting of the array for the labels happens outside the useEffect that compiles the conditional state.
If I'm wrong on my assessment please let me know, and if you have any ideas on how to better structure this, I'd love to hear it. I've looked at useReducer as a possible alternative, but I'm not that familiar with the react hooks beyond useState and useEffect, and so far haven't determined how to write out the reducer, again I keep coming back to needing to have the conditional pieces of state available at the right place in the code.

React - How to use the current state value as a variable so i can set as a index inside an array

I am pretty a new to React and currently to use the current state of and object as a variable in another array, said array will start filled with 0, and every time someone press the vote button it will store +1 to said array index. I am sure its the wrong way but nevertheless I amtrying to figure out if is possible to use the logic i created.
Thanks for the patience!
import React, { useState } from 'react'
const App = () => {
const anecdotes = [
'If it hurts, do it more often',
'Adding manpower to a late software project makes it later!',
'The first 90 percent of the code accounts for the first 10 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.',
'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
'Premature optimization is the root of all evil.',
'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.',
'Programming without an extremely heavy use of console.log is same as if a doctor would refuse to use x-rays or blood tests when diagnosing patients'
]
const [selected, setSelected] = useState(0)
var ary = new Uint8Array(10);
//console.log('this', this.state);
//show 1 anecdote
//when user press next its generates a random number beetween 0 and 6 to display the anecdote
//issue:
//how to add said random value to my arrays so I can use the array to store the votes of each anecdote
return (
<div>
<h1>{anecdotes[selected]}</h1>
<button onClick={ () => setSelected(Math.floor(Math.random() * 6) ) }>Next Anecdote</button>
<button onClick={ () => ary[selected.state] + 1 }>Vote</button>
<p>votes: {ary[selected.state]}</p>
</div>
)
}
export default App
First, you'll need an array to hold the vote count values, and secondly, correctly update each vote count in an immutable update.
export default function App() {
const [selected, setSelected] = useState(0);
// create vote counts array from anecdotes array and initialize to zeros
const [votes, setVotes] = useState(Array.from(anecdotes).fill(0));
return (
<div>
<h1>{anecdotes[selected]}</h1>
<button
onClick={() => setSelected(
// use anecdote array length
Math.floor(Math.random() * anecdotes.length))
}
>
Next Anecdote
</button>
<button
onClick={() =>
setVotes((votes) =>
// immutable update, map previous state to next
votes.map((count, index) =>
index === selected ? count + 1 : count
)
)
}
>
Vote
</button>
<p>votes: {votes[selected]}</p> // display selected anecdote vote count
</div>
);
}
All values which you change in React should be reactive, you never should change a value directly, because it will not trigger re-rendering. You should use useState hook.
In your case to store the anecdotes votes you could create a new array with length 6 and fill it with initial votes count - 0. Then you should call the hook to update the counts.
const [votes, setVotes] = useState(new Array(6).fill(0));
return (
<div>
<h1>{anecdotes[selected]}</h1>
<button onClick={ () => setSelected(Math.floor(Math.random() * 6) ) }>Next Anecdote</button>
<button onClick={ () => { setVotes(prevVotes => {
const upd = [...prevVotes];
upd[selected] += 1;
return upd;
})} }>Vote</button>
<p>votes: {votes[selected]}</p>
</div>
)
I think, using useReducer will help you keep everything in one place:
import React, { useState, useReducer } from "react";
const initialState = [
{ text: "If it hurts, do it more often", votes: 0 },
{
text: "Adding manpower to a late software project makes it later!",
votes: 0
},
{
text:
"The first 90 percent of the code accounts for the first 10 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.",
votes: 0
},
{
text:
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand.",
votes: 0
},
{ text: "Premature optimization is the root of all evil.", votes: 0 },
{
text:
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.",
votes: 0
},
{
text:
"Programming without an extremely heavy use of console.log is same as if a doctor would refuse to use x-rays or blood tests when diagnosing patients",
votes: 0
}
];
const reducer = (state, action) => {
if (action.type === "VOTE_UP") {
return state.map((item, index) => {
if (index === action.index) {
item.votes = item.votes + 1;
}
return item;
});
}
};
const App = () => {
const [anecdotes, dispatch] = useReducer(reducer, initialState);
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div>
<h1>{anecdotes[selectedIndex].text}</h1>
<button
onClick={() => {
setSelectedIndex(Math.floor(Math.random() * 6));
}}
>
Next Anecdote
</button>
<button
onClick={() => {
dispatch({ type: "VOTE_UP", index: selectedIndex });
}}
>
Vote
</button>
<p>votes: {anecdotes[selectedIndex].votes}</p>
</div>
);
};
export default App;

React js - Array doesn't update after a onSwipe() event

I am a Beginner to Reactjs and I just started working on a Tinder Clone with swipe functionality using tinde-card-react.
I am trying to get two variables to update using React useState() but coudn't.
There are 2 main components inside the main function, a TinderCards component and Swipe right and left and Replay buttons. The problem is that when I swipe the cards manually variables don't get updated and this is not the case when i swipe using the buttons.
In the current log, I swiped the cards twice to the right and logged the variables alreadyRemoved and people. The variable people is initially an Array containing 3 objects so after the second swipe it's supposed to log only 2 objects not 3, While the alreadyRemoved variable is supposed to update to the missing elements of the variable people.
This is my code :
import React, { useState, useEffect, useMemo } from 'react';
import './IslamCards.css';
import Cards from 'react-tinder-card';
import database from './firebase';
import hate from "./Cross.png"
import replayb from "./Replay.png"
import love from "./Love.png"
import IconButton from "#material-ui/core/IconButton"
function IslamCards(props) {
let [people, setPeople] = useState([])
useEffect(() => {
database.collection("People").onSnapshot(snapshot => { setPeople(snapshot.docs.map(doc => doc.data())) })
}, [])
let [alreadyRemoved , setalreadyRemoved] = useState([])
let buttonClicked = "not clicked"
// This fixes issues with updating characters state forcing it to use the current state and not the state that was active when the card was created.
let childRefs = useMemo(() => Array(people.length).fill(0).map(() => React.createRef()), [people.length])
let swiped = () => {
if(buttonClicked!=="clicked"){
console.log("swiped but not clicked")
if(people.length){
let cardsLeft = people.filter(person => !alreadyRemoved.includes(person))
if (cardsLeft.length) {
let toBeRemoved = cardsLeft[cardsLeft.length - 1] // Find the card object to be removed
let index = people.map(person => person.name).indexOf(toBeRemoved.name)// Find the index of which to make the reference to
setalreadyRemoved(list => [...list, toBeRemoved])
setPeople(people.filter((_, personIndex) => personIndex !== index))
console.log(people)
console.log(alreadyRemoved)
}
}
buttonClicked="not clicked"
}
}
let swipe = (dir) => {
buttonClicked="clicked"
console.log("clicked but not swiped")
if(people.length){
let cardsLeft = people.filter(person => !alreadyRemoved.includes(person))
if (cardsLeft.length) {
let toBeRemoved = cardsLeft[cardsLeft.length - 1] // Find the card object to be removed
let index = people.map(person => person.name).indexOf(toBeRemoved.name)// Find the index of which to make the reference to
setalreadyRemoved(list => [...list, toBeRemoved])
childRefs[index].current.swipe(dir)
let timer =setTimeout(function () {
setPeople(people.filter((_, personIndex) => personIndex !== index))}
, 1000)
console.log(people)
console.log(alreadyRemoved)
}
// Swipe the card!
}
}
let replay = () => {
let cardsremoved = alreadyRemoved
console.log(cardsremoved)
if (cardsremoved.length) {
let toBeReset = cardsremoved[cardsremoved.length - 1] // Find the card object to be reset
console.log(toBeReset)
setalreadyRemoved(alreadyRemoved.filter((_, personIndex) => personIndex !== (alreadyRemoved.length-1)))
if (!alreadyRemoved.length===0){ alreadyRemoved=[]}
let newPeople = people.concat(toBeReset)
setPeople(newPeople)
// Make sure the next card gets removed next time if this card do not have time to exit the screen
}
}
return (
<div>
<div className="cardContainer">
{people.map((person, index) => {
return (
<Cards ref={childRefs[index]} onSwipe={swiped} className="swipe" key={index} preventSwipe={['up', 'down']}>
<div style={{ backgroundImage: `url(${person.url})` }} className="Cards">
<h3>{person.name}</h3>
</div>
</Cards>);
})}
</div>
<div className="reactionButtons">
<IconButton onClick={() => swipe('left')}>
<img id="hateButton" alt="d" src={hate} style={{ width: "10vh", marginBottom: "5vh", pointerEvents: "all" }} />
</IconButton>
<IconButton onClick={() => replay()}>
<img id="replayButton" alt="e" src={replayb} style={{ width: "11vh", marginBottom: "5vh", pointerEvents: "all" }} />
</IconButton>
<IconButton onClick={() => swipe('right')}>
<img id="loveButton" alt="f" src={love} style={{ width: "11vh", marginBottom: "5vh", pointerEvents: "all" }} />
</IconButton>
</div>
</div>
);
}
export default IslamCards;
My console Log :
UPDATE :
As suggested in the 1st answer, I removed the Timer from the swiped() function but the problem persisted.
I hope to get more suggestions, so that I can solve this problem.
I can see the problem, but you might need to figure out what to do after that.
setPeople(people.filter((_, personIndex) => personIndex !== index))}
, 1000)
The problem is that index is figured out from the current update, however it takes 1 second to reach the next update, in between, your index points to the same one, because your index is derived from the people.

Creating different instances of a React component

My question really simple: how do I create different instances of a React component?
I am doing an exercise in which you have to create a voting system: each component has its own quantity of votes.
The issue that I am having is that each component share the same number of votes, instead of being separate.
Here is the code:
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
const Anecdote = ({text}) =>
{
const [votes, setVotes] = useState(0);
return (
<React.Fragment>
<p>{text}</p>
<p>Votes: {votes}</p>
<button onClick={() => setVotes(votes + 1)}>vote</button>
</React.Fragment>
)
}
const App = (props) =>
{
const [selected, setSelected] = useState(0);
function randomizeAnecdote(){
setSelected(Math.floor(Math.random() * anecdotes.length));
}
return (
<div>
{props.anecdotes[selected]}
<br/>
<button onClick={() => randomizeAnecdote()}>press</button>
</div>
)
}
const anecdotes = [
React.createElement(Anecdote, {text:'If it hurts, do it more often'}),
React.createElement(Anecdote, {text:'Adding manpower to a late software project makes it later!'}),
React.createElement(Anecdote, {text:'The first 90 percent of the code accounts for the first 90 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.'}),
React.createElement(Anecdote, {text:'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.'}),
React.createElement(Anecdote, {text:'Premature optimization is the root of all evil.'}),
React.createElement(Anecdote, {text:'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.'}),
]
ReactDOM.render(
<App anecdotes={anecdotes} />,
document.getElementById('root')
)
Basically, the function randomizeAnecdote() chooses a random anecdote to be displayed with its own text. However, even when displaying another anecdote, the votes don't change.
As an example, if one anecdote has 10 votes and I press the button to randomize, the 10 votes stay there.
How can I make it so that votes is unique to each element?
To reset the vote, you can listen on text in useEffect and whenever its changed, set vote to 0.
useEffect(() => {
setVotes(0)
}, [ text ])
Also, while testing I found an issue that random value is the same as previous value. So for that, you can use following hack:
function randomizeAnecdote(){
let randomValue = Math.floor(Math.random() * anecdotes.length);
randomValue = (randomValue === selected ? randomValue + 1 : randomValue) % anecdotes.length;
setSelected(randomValue);
}
Following is a sample code:
Note it addresses following things:
Reset vote count on new text.
Fixed Randomize function so no value is repeated
Updated code to save strings in array instead of React.Element
const { useState, useEffect } = React;
const Anecdote = ({text}) => {
const [votes, setVotes] = useState(0);
useEffect(() => {
setVotes(0)
}, [ text ])
return (
<React.Fragment>
<p>{text}</p>
<p>Votes: {votes}</p>
<button onClick={() => setVotes(votes + 1)}>vote</button>
</React.Fragment>
)
}
const App = ({anecdotes}) => {
const [selected, setSelected] = useState(0);
function randomizeAnecdote(){
let randomValue = Math.floor(Math.random() * anecdotes.length);
randomValue = (randomValue === selected ? randomValue + 1 : randomValue) % anecdotes.length;
setSelected(randomValue);
}
return (
<div>
<Anecdote text={ anecdotes[selected] } />
<br/>
<button onClick={() => randomizeAnecdote()}>press</button>
</div>
)
}
const anecdotes = [
'If it hurts, do it more often',
'Adding manpower to a late software project makes it later!',
'The first 90 percent of the code accounts for the first 90 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.',
'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
'Premature optimization is the root of all evil.',
'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.',
]
ReactDOM.render(
<App anecdotes={anecdotes} />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id='root' />
Updated code to maintain the count:
The reason its resetting to 0 is because the useEffect is setting votes to 0 on change of text. If you need to maintain count, you will have to maintain a complex state.
In following sample, state is of type:
[ key: string ]: number
where key is the text and value is the count.
In ideal env, I would create a redux store that would maintain both values in more detailed fashion. But for sample, you can create a map<text, vote> and use it to display/maintain count.
const { useState, useEffect } = React;
const Anecdote = ({text}) => {
const [ myState, setMyState ] = useState({})
useEffect(() => {
if ( !myState[ text ] ) {
const state = { ...myState }
state[ text ] = 0;
setMyState(state);
}
}, [ text ])
const incrVote = () => {
const state = { ...myState }
state[ text ] = (state[ text ] || 0) + 1;
setMyState(state);
}
return (
<React.Fragment>
<p>{text}</p>
<p>Votes: {myState[ text ] || 0}</p>
<button onClick={incrVote}>vote</button>
</React.Fragment>
)
}
const App = ({anecdotes}) => {
const [selected, setSelected] = useState(0);
function randomizeAnecdote(){
let randomValue = Math.floor(Math.random() * anecdotes.length);
randomValue = (randomValue === selected ? randomValue + 1 : randomValue) % anecdotes.length;
setSelected(randomValue);
}
return (
<div>
<Anecdote text={ anecdotes[selected] } />
<br/>
<button onClick={() => randomizeAnecdote()}>press</button>
</div>
)
}
const anecdotes = [
'If it hurts, do it more often',
'Adding manpower to a late software project makes it later!',
'The first 90 percent of the code accounts for the first 90 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.',
'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
'Premature optimization is the root of all evil.',
'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.',
]
ReactDOM.render(
<App anecdotes={anecdotes} />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id='root' />
It seems a risky way to handle the count: I would never rely on the state of a "temporary" component for something important, since it makes things difficult for both persistency and tracking. I'm not familiar with useState, but obviously there is something wrong with the Javascript closures.
I'd separate data and components (you have together now), keeping track of the count in the higher component possible (App, in your case, if you switch to redux it simplifies things) and creating the Anecdote on the fly. That would be an easier to manage option, imho.
If I would write the code, I'd tackle it differently. That is subjective, so don't take it as correct or wrong (I don't call myself an expert at all), but I'll put some comments for my thoughts.
import React from 'react';
import ReactDOM from 'react-dom';
// Anecdote is simple, there is no state, only rendering
// <> is a shortcut for <React.Fragment>, can't use in StackOverflow
const Anecdote = ({text, votes, incVotes}) =>
<React.Fragment>
<p>{text}</p>
<p>Votes: {votes}</p>
<button onClick={() => incVotes()}>vote</button>
</React.Fragment>
// Data and components are separate, I don't merge them
const anecdotes = [
'If it hurts, do it more often',
'Adding manpower to a late software project makes it later!',
'The first 90 percent of the code accounts for the first 90 percent of the development time...The remaining 10 percent of the code accounts for the other 90 percent of the development time.',
'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.',
'Premature optimization is the root of all evil.',
'Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.',
]
// I'd go for a standard declaration of class, at least for the App
class App extends React.Component {
// I'm not familiar with useState (my bad), so here a classic init for the state
// (very verbose, I know)
constructor(props) {
super(props);
// Bogus: it starts with 0, can be fixed obvs
this.state = { selected: 0, votesCount: props.anecdotes.map(() => 0) };
}
// Your function, now external, I find it more readable. It could be improved.
randomizeAnecdote() {
const selected = Math.floor(Math.random() * anecdotes.length);
setState({ selected });
}
// I'd use the callback for the state, in case multiple click occurs and
// React groups the calls.
// Note that I copy the array, this will simplify the transition to redux.
// Using "immutable" behaviour is a good habit and simplifies debug.
incVotes() {
this.setState(prevState => {
const votesCount = [...prevState.votesCount];
votesCount[prevState.selected]++;
return({ ...prevState, votesCount });
});
}
// Much simpler render, there is no more array of Anecdote
render() {
return (
<div>
<Anecdote
text={this.props.anecdotes[selected]}
votes={this.state.votesCount[selected]}
incVotes={() => this.incVotes()}
/>
<br/>
<button onClick={() => this.randomizeAnecdote()}>press</button>
</div>
);
}
}
ReactDOM.render(
<App anecdotes={anecdotes} />,
document.getElementById('root')
)
It might not reply to your answer (because I don't get which closure exactly is broken), but with an approach like above it should be easier to debug and maintain.

Reset state on prop change

Lets say I have these two components:
const availableTasks = [1, 2, 3];
const Task = () => {
const [current, setCurrent] = React.useState(0);
const getNextTask = () => current + 1 < availableTasks.length ? current + 1 : 0;
return (
<div className='task-container'>
<div className='task-information'>
Some information.
</div>
<TaskVote id={availableTasks[current]} key={current}/>
<button onClick={() => setCurrent(getNextTask())}> Next Task</button>
</div>
);
};
const TaskVote = ({ id }) => {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setInterval(() => setCount(count => count + 1), 1000); // Async data receiving (e.g. websocket)
}, []);
return <div> Counting for: {id}, Counted: {count}</div>;
};
TaskVote receives its data from websockets, and updates count state when needed. (Replaced it with interval, for the example)
Task renders some information about the task, it renders TaskVote, and a "Next Task" button.
When user is skipping to the next task, TaskVote receives new key, so it will re-mount, which is fine.
In case there is only one element in availableTaks, the key prop won't change, so count state will not reset, even though I want it to reset once the button is clicked (even if it's the same Task).
How can I handle this case?
Thanks!
Your problem is that you use a simple number as the key which does not update if your next task is the same as before.
This leads to the following rendering multiple times:
<TaskVote id={1} key={0}/>
React cannot determine that something changed.
A simple solution is to keep a separate state which can be used as a key.
const [voteState, setVoteState] = useState(0)
...
<TaskVote id={availableTasks[current]} key={voteState}/>
<button onClick={() => {
setCurrent(getNextTask())
setVoteState((s) => s+1) // always increasing
}}> Next Task</button>
However, setting multiple state values can lead to multiple rerenders.
You can optimize this by merging multiple values (or by useReducer):
const [current, setCurrent] = React.useState({ task: 0, voteButtonState: 0 });
...
<TaskVote id={availableTasks[current]} key={current.voteButtonState}/>
<button onClick={() => {
setCurrent((old) => {
return {
task: getNextTask(),
voteButtonState: old.voteButtonState + 1 // always increasing
}
})
}> Next Task</button>

Categories