The buttons i create using below seems to lag in the selectedButtonIdx value.
Is the toggleSelected not complete by the time getClass is called ?
function ButtonGroup(props) {
const [selectedButtonIdx,setIdx]=useState(props.loadCurrentAsIndex);
const toggleSelected = (e) => {
setIdx(parseInt(e.target.dataset.index));
props.onclick(e);
};
const getClass = (index) => {
return (selectedButtonIdx === index) ? classnames('current', props.btnClass)
: classnames(props.btnClass)
};
let buttons = props.buttons.map((b, idx) => <Button key={idx} value={b.value} index={idx} text={b.text}
onclick={e => toggleSelected(e)}
btnClass={getClass(idx)}/>);
return (
<div>
{buttons}
</div>
);
}
Every onclick is expected to show the user which button in the group was clicked by changing its class.
By looking at this,
<Button
key={idx}
value={b.value}
index={idx}
text={b.text}
onclick={e => toggleSelected(e)}
btnClass={getClass(idx)}
/>
Button is your custom component,
Two things to notice here,
You have provided onclick (c is small) props, in you actual component it should be onClick={props.onclick}
You have used e.target.dataset.index, to work with dataset we should have attribute with data- prefix. So your index should be data-index in your actual component.
So finally your Button component should be,
const Button = (props) => {
return <button text={props.text} data-index={props.index} onClick={props.onclick} className={props.btnClass}>{props.value}</button>
}
Demo
The function setIdx, returned from useState is asynchronous, this means that it may be not be finished by the time you run your next function (as you guessed).
Take a look at useEffect it allows you to specify a function to run once an item in your state changes, this method will ensure your functions are called in the right order.
By now I don't see anything wrong here.
How it works:
initial render happens, onClick event listener is bound
user clicks a button, event handler calls setIdx triggering new render
new render is initiated, brand new selectedButtonIdx is used for rendering(and for getClass call as well)
See, there is no reason to worry about if setIdx is sync function or async.
Related
I am making a calculator in react in which i made buttons for numbers and when button "7" is pressed then in the input field 7 is added.
My approach:
I am using useState to do this.
I made an arrow function funinpval which takes takes number as string in argument then i am using this function with different buttons onclick handler by passing respective numbers as arguments. But I am getting error
import React from 'react'
import { useState } from 'react';
export const Calculator = () => {
const [inpval, setInpval] = useState("")
const funinpval = (num) => {
setInpval(inpval + num)
}
return(
<>
<input type="text" value={inpval}>
<button onClick={funinpval("7")}>7</button>
<button onClick={funinpval("8")}>8</button>
</>
)
Can anyone please help
<button onClick={funinpval("7")}>7</button>
<button onClick={funinpval("8")}>8</button>
You are not waiting the user to click the buttons to execute the functions, they are instead executed every render phase, directly. Which mean that the component render -> state update -> new re-render -> new state update -> ...
To fix it:
<button onClick={() => funinpval("7")}>7</button>
<button onClick={() => funinpval("8")}>8</button>
There is a syntax error in how you are providing the event handlers.
You have to provide event handlers sonething like:
<button onClick={() => funinpval("7")}>7</button>
<button onClick={() => funinpval("8")}>8</button>
Simply writing onClick={funinpval("7")} will immediately call the function while rendering which sets the state. When state got updated then the component re-renders. Then again while re-rendering, this function got called and so on.
onClick={funinpval("7")}
will return the result of calling that function to the listener rather than a reference to the function that the listener can call. So you're setting state immediately with those two buttons which is causing the render which is calling the function again which is setting the state again... infinity!
In this example I pick up the textContent of the button and use that to set the new input state, and then you can simply just pass the reference to the function to the handler and let the function deal with how state is set.
const { useState, useEffect } = React;
function Calulator() {
const [inpval, setInpval] = useState(0);
function funinpval(e) {
// Grab the `textContent` of the button and
// relabel it to `num` making sure to coerce the
// text to a number first
const { textContent: num } = e.target;
setInpval(inpval + Number(num));
}
return(
<div>
<input type="text" value={inpval} />
<button onClick={funinpval}>7</button>
<button onClick={funinpval}>8</button>
</div>
)
};
// Render it
ReactDOM.render(
<Calulator />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
function setTodoInfo(id) {
let todoInfo = todoInfoRef.current.value
if(todoInfo === "") return
todo.info = todoInfo
todoInfoRef.current.value = null
}
<>
<h1 className="delete-button"> hi, {todo.info} </h1>
<form>
<input type="text" ref={todoInfoRef}/>
</form>
<button onClick={closeInfo} className="delete-button" > Close </button>
<button onClick={setTodoInfo}> Set Info</button>
</>
When I click the set Info button its updating the info property on the todo, but it doesn't display it when you click, you have to close it and reopen it to see the updated info
react uses reference to be able to see it should or not rerender. using refs means the reference doesnt change...
as pointed, you should really see the docs on how to make it the "react way"
if you really need to make with references, then you could add some "render" function.
put in a useState a integer or something else, then call setState to change its value... that should force a render.
function useForceUpdate(){
const [value, setValue] = useState(0); // integer state
return () => setValue(value => value + 1); // update the state to force
}
(inside functional component)
const forceUpdate = useForceUpdate();
call forceUpdate where needed
I am using a component that I cannot change directly, but I would like to extend.
import { Button } from '#external-library'
// Currently how the button component is being used
<Button click={() => doSomething()} />
// I would like to add a tabIndex to the button
<Button click={() => doSomething()} tabIndex={0} />
I cannot add an attribute because the component is not expecting a tabIndex. I cannot directly modify the Button component.
How can I extend the <Button /> component so I can add attributes like tabIndex, etc?
I was hoping something like the following would work:
export default class ExtendedButton extends Button { }
// except I'm dealing with functional components
You can't edit custom component implementation without changing its internals.
// You can't add tabIndex to internal button without changing its implementation
const Button = () => <button>Click</button>;
In such cases, you implement a wrapper with desired props:
const Component = () => {
return (
<div tabIndex={0}>
<Button />
</div>
);
};
If the component forwarding ref (also depends to which element it forwarded in the implementation), you can use its attributes:
// Assumption that Button component forwards ref
const Button = React.forwardRef((props,ref) => <button ref={ref}>Click</button>);
<Button ref={myRef}/>
// Usage
myRef.current.tabIndex = 0;
You can access the inner DOM button element using React refs(read here)
most likely the external-lib you use provide a ref prop for the Button component which you use to pass your own create ref
const buttonRef = useRef(null);
<Button ref={buttonRef}/>
Then you can use buttonRef.current to add tabIndex when your data is ready to be populated in like
useEffect( () => {
if(buttonRef && buttonRef.current){
buttonRef.current.tabIndex = 2;
}
}, [props.someProperty] );
i'm trying to develop an App with React using the Open trivia Api. I have mapped a button component (using material ui) to show the different answers for each question. I'm struggling now to target only the clicked one to apply a css property: if the answer is correct should become green, else red. The problem is the fact that once i click, all button become red or green. I tried to store the index in a state and compare the real index, but it doesn't work. here is my code:
in the main APP.js
const [clickedOne, setClickedOne] = useState({
clickedIndex: null,
});
useEffect(() => {
grabData();
}, []);
const handleClick = (choice, ke) => {
setChoice(choice);
if (choice === data.correct_answer) {
setIsCorrect(true);
} else {
setIsCorrect(false);
}
setClickedOne({ clickedIndex: ke });
grabData();
};
The mapped button inside the Render:
{answers.map((answer, index) => {
return (
<ContainedButtons
choice={handleClick}
answer={answer}
correct={data.correct_answer}
isCorrect={isCorrect}
key={index}
id={index}
clicked={clickedOne}
/>
);
})}
Inside the Button component:
const backStyle = () => {
if (clicked === id) {
if (isCorrect) {
return "green";
} else if (isCorrect === false) {
return "red";
} else {
return null;
}
}
};
return (
<div className={classes.root}>
<Button
style={{ backgroundColor: backStyle() }}
value={answer}
onClick={() => choice(answer, id)}
variant="contained"
>
{decodeURIComponent(answer)}
</Button>
When i check now inside the backstyle function if the clicked===id, now nothing happens anymore. Without that if check, i would have all buttons red or green.
Thank you guys for the help!
I have looked at your codesandbox demo, there are alot of other problems apart from the one your question is about.
First of all, each time you make a request to the API to fetch next question, you are making a request to get 10 questions instead of 1. API request URL contains a query parameter named amount which determines how many questions will be fetched on each request. Change its value to 1.
"https://opentdb.com/api.php?amount=1&encode=url3986"
Secondly, there is a lot of unnecessary code and unnecessary use of useState hook. You only need 2 things to be stored in the state, data and answers
const [data, setData] = useState({});
const [answers, setAnswers] = useState([]);
Now, coming to the original problem of detecting which button is clicked and correctly updating its background color.
To achieve the desired functionality, take following steps:
create couple of CSS classes as shown below
button.bgGreen {
background-color: green !important;
}
button.bgRed {
background-color: red !important;
}
pass a handleClick function from App component to ContainedButtons component. When a button is clicked, this click handler will be invoked. Inside the handleClick function, get the text and the button that was clicked using Event.target and depending on whether user answered correctly or not, add appropriate CSS class, created in step 1, on the button that was clicked.
Instead of using index as key for ContainedButtons in map function, use something that will be unique each time. This is needed because we want React to not re-use the ContainedButtons because if React re-uses the ContainedButtons component, then CSS classes added in step 2 will not be removed from the button.
Here's a working codesanbox demo of your app with the above mentioned steps.
In this demo, i have removed the unnecessary code and also changed the key of ContainedButtons inside map function to key={answer.length * Math.random() * 100}. You can change it to anything that will ensure that this key will be unique each time.
Edit: forgot an important part - this is noticeable if you click the button next to Jeff A. Menges and check the console log.
The important part of the code is the "setFullResults(cardResults.data.concat(cardResultsPageTwo.data))" line in the onClick of the button code. I think it SHOULD set fullResults to whatever I tell it to... except it doesn't work the first time you click it. Every time after, it works, but not the first time. That's going to be trouble for the next set, because I can't map over an undefined array, and I don't want to tell users to just click on the button twice for the actual search results to come up.
I'm guessing useEffect would work, but I don't know how to write it or where to put it. It's clearly not working at the top of the App functional component, but anywhere else I try to put it gives me an error.
I've tried "this.forceUpdate()" which a lot of places recommend as a quick fix (but recommend against using - but I've been trying to figure this out for hours), but "this.forceUpdate()" isn't a function no matter where I put it.
Please help me get this button working the first time it's clicked on.
import React, { useState, useEffect } from "react";
const App = () => {
let artistData = require("./mass-artists.json");
const [showTheCards, setShowTheCards] = useState();
const [fullResults, setFullResults] = useState([]);
useEffect(() => {
setFullResults();
}, []);
let artistDataMap = artistData.map(artistName => {
//console.log(artistName);
return (
<aside className="artist-section">
<span>{artistName}</span>
<button
className="astbutton"
onClick={ function GetCardList() {
fetch(
`https://api.scryfall.com/cards/search?unique=prints&q=a:"${artistName}"`
)
.then(response => {
return response.json();
})
.then((cardResults) => {
console.log(cardResults.has_more)
if (cardResults.has_more === true) {
fetch (`https://api.scryfall.com/cards/search?unique=prints&q=a:"${artistName}"&page=2`)
.then((responsepagetwo) => {
return responsepagetwo.json();
})
.then(cardResultsPageTwo => {
console.log(`First Results Page: ${cardResults}`)
console.log(`Second Results Page: ${cardResultsPageTwo}`)
setFullResults(cardResults.data.concat(cardResultsPageTwo.data))
console.log(`Full Results: ${fullResults}`)
})
}
setShowTheCards(
cardResults.data
.filter(({ digital }) => digital === false)
.map(cardData => {
if (cardData.layout === "transform") {
return (
//TODO : Transform card code
<span>Transform Card (Needs special return)</span>
)
}
else if (cardData.layout === "double_faced_token") {
return (
//TODO: Double Faced Token card code
<span>Double Faced Token (Needs special return)</span>
)
}
else {
return (
<div className="card-object">
<span className="card-object-name">
{cardData.name}
</span>
<span className="card-object-set">
{cardData.set_name}
</span>
<img
className="card-object-img-sm"
alt={cardData.name}
src={cardData.image_uris.small}
/>
</div>
)
}
})
)
});
}}
>
Show Cards
</button>
</aside>
);
});
return (
<aside>
<aside className="artist-group">
{artistDataMap}
</aside>
<aside className="card-wrapper">
{showTheCards}
</aside>
</aside>
);
};
export default App;
CodesAndBox: https://codesandbox.io/embed/compassionate-satoshi-iq3nc?fontsize=14
You can try refactoring the code like for onClick handler have a synthetic event. Add this event Listener as part of a class. Use arrow function so that you need not bind this function handler inside the constructor. After fetching the data try to set the state to the result and use the state to render the HTML mark up inside render method. And when I run this code, I have also seen one error in console that child elements require key attribute. I have seen you are using Array.prototype.map inside render method, but when you return the span element inside that try to add a key attribute so that when React diffing algorithm encounters a new element it reduces the time complexity to check certain nodes with this key attribute.
useEffect(() => {
// call the functions which depend on fullResults here
setFullResults();
}, [fullResults])
// now it will check whether fullResults changed, if changed than call functions inside useEffect which are depending on fullResults