How to set to initial state in React Hooks - javascript

I have an array that creates a mapping of items with checkboxes. each item has a checked state:
const [checked, setChcked] = React.useState(false)
So the user checks say 5 out of the 20 checkboxes and then press a button (the button is in the higher component, where there is a mapping that creates this items with checkboxes) and it works as intended. But, after the button is pressed and the modal is closing, after I open the modal again, these 5 checkboxes are still checked. I want them to restart to be unchecked just like when I refresh and the state vanishes. Now, I am aware of techniques such as not saving state per each item and just saving the state of the array of items in the higher component but I am confused as I have heard that hooks were created so that it is good practice to sometime save state in dumb components.
Is there a simpler function to just restart to initial value?
Edit:
adding the code
<div>
{policyVersionItems.map(item=> (
<PolicyVersionItem
key={pv.version}
policyVersionNumber={item.version}
policyVersionId={item._id}
handleCheck={handleCheck}
>
{' '}
</PolicyVersionItem>
))}
</div>
And the item
const PolicyVersionItem: React.FunctionComponent<PolicyVersionItemProps> = props => {
const { , policyVersionNumber, policyVersionId, handleCheck } = props
const [checked, setChcked] = React.useState(false)
return (
<Wrapper>
<Label dark={isEnabled}> Version {policyVersionNumber}</Label>
<Checkbox
checked={checked}
onClick={() => {
if (isEnabled || checked) {
setChcked(!checked)
handleCheck(policyVersionId, !checked)
}
}}
/>
</Wrapper>
)
}
Some of it is not relevant. the handle check function is a function that returns data to the higher component from the lower component for example.

Related

useState to check if all radio boxes selected

I in my React Application, I have a set of mapped questions from backend data where the user selects a response with a radio button:
<RadioGroup row>
{data.response.map((dt, rIndex) => {
return (
<div className="answerContainer">
<FormControlLabel
className="MuiTypography-body1"
value={dt.value}
onChange={() => {
debugger;
setAnswer(
dt.value,
data.questionTitle,
qIndex,
rIndex
);
}}
checked={selectedAnswers[qIndex] === rIndex}
control={
<Radio
className="PrivateRadioButtonIcon-root-9"
required={true}
/>
}
label={dt.value}
/>
</div>
);
})}
</RadioGroup>
I want to disable the "next" navigation button unless all answers are checked off. I created a state called proceed which by default is false:
const [proceed, setProceed] = React.useState(false);
In my handleChange event if the number of questions is less than the number of answers, the button is disabled.
const setAnswers = async () => {
if (questionsData.length < selectedAnswers.length) {
setProceed(false);
return;
}
Then I added this statement in my setAnswers handleChange function to check if the user can proceed to the next page in the stepper:
if (questionsData.length === selectedAnswers.length) {
setProceed(true);
}
Finally I pass the handleChange function into my button:
<button
className={proceed ? 'enabled' : 'disabled'}
onClick={() => {
setAnswers();
}}
> Next </Button>
The setProceed === false condition works correctly. set proceed === true appeared to be working, but I discovered if I clicked the last answer in the questions without clicking the others, setProceed is === true is triggered and the button is enabled, allowing users to skip the questions.
I added the required flag to the MUI radio button, but the function bypasses it.
Similar to this answer on S/O (which is related to PHP), How can I ensure that all answers must be selected before enabling this state change in React?
Instead of using the length to compare if the all the values are filled . you can use the every from Array.
If your selected Answers contains a list of boolean ( [ true, false ] ) values then you can do
const isAllAnswersSelected = selectedAnswers.every(Boolean)
isAllAnswersSelected will be true if all the items in the selectedAnswers is true else it will return false.
Refer
Array Every
Material UI doesn't provide validations and you must take care of validations and error state yourself.
required form attribute adds the asterisks to the label and not the validations.
<Radio className="PrivateRadioButtonIcon-root-9" required={true} error={questionHasError(qIndex)}/> will again add style level error indicators.
You can maintain a state of answered questions instead?
const [answeredQuestions, setAnsweredQuestions] = useState([]);
.
.
.
const setAnswers = async (qIndex) => {
const updatedAnsweredIdx = [...answeredQuestions, qIndex];
setAnsweredQuestions(updatedAnsweredIdx );
// check if all the questions have been selected/answered
if(allQuestionIndex.every(idx => updatedAnsweredIdx.includes(idx))){
setProceed(true);
} else {
setProceed(false);
}
}
You can then enhance validations by tracking if question is answered and pass error prop to highlight error state.

Is it possible to pass a React Component the same function that created it?

I'm trying to create a Pokedex web app. I have a Card component that displays an individual Pokemon, and within that I have navigation buttons to go back and forth down the list of Cards, and one to exit from the card. I have a function called createCard in the parent component that creates a card in the parent Component.
createCard = (key) => {
const currentCard = (
<Card className="Card"
key={Pokemon[key].key}
id={Pokemon[key].id}
name={Pokemon[key].name}
types={Pokemon[key].types}
img={Pokemon[key].img}
stats={Pokemon[key].stats}
dexEntry={Pokemon[key].dexEntry}
cry={Pokemon[key].cry}
abilities={Pokemon[key].abilities}
sprite={Pokemon[key].sprite}
hideCard={this.hideCard}
createTabs={this.createTabs}
createCard={this.createCard}
/>
)
this.setState({
currentScreen: currentCard
})
}
My issue is that the 'next' and 'previous' navigation buttons don't work (The exit one works fine). The way I set them up is that onclick, they should call the createCard function based on the Pokemon in my dictionary that's key value is one higher or lower than the current one.
<div className="navButtons">
<button className="navBtn" onClick={() => props.createCard(props.key - 1)}>❮</button>
<button className="navBtn" onClick={() => props.hideCard()}>✖</button>
<button className="navBtn" onClick={() => props.createCard(props.key + 1)}>❯</button>
</div>
When I push one of the navigation buttons though, I get "TypeError: Cannot read property 'id' of undefined" and it points to my createCard function. This is my first question on here, so I apologize if it's too lengthy, doesn't contain enough data, or something of the sort.

Targeting only the clicked item on a mapped component ( React quiz trivia App)

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.

Cells overlapping problems in react virtualized with cell measurer

I am using React Virtualized to window my items list which has the following functionality
1) onClicking on any item, the user will be prompted with a details panel where he will be able to update the details of the item. And when he goes back to the list, he will be able to see the details in the item cell in the list. He can add a lot of details to make it bigger or reduce the size by removing the details
2) He can delete the item, or add another item to the list with certain details or without details.
CellMeasurer serves my requirement as dynamic height is supported. But I am having following issues with it
1) initially when my list mounts for the first time, first few items are measured and shown correctly but as soon as I scroll to the end, items get overlapped with each other.(positining isnt correct, I am guessing the defaultHeight is being applied to the unmeasured cells). This works fine as soon as the list is rerendered again.
2) Also, when I am updating the details of an item the list doesnt update with the new height adjustments
I am sure that somewhere my implementation is incorrect but have spent a lot of time hammering my head for it. Please let me know what can be done here to fix this
ItemView = props => {
const {index, isScrolling, key, style} = props,
items = this.getFormattedItems()[index]
return (
<CellMeasurer
cache={this._cache}
columnIndex={0}
isScrolling={isScrolling}
key={key}
rowIndex={index}
parent={parent}>
{({measure, registerChild}) => (
<div key={key} style={style} onLoad={measure}>
<div
onLoad={measure}
ref={registerChild}
{...parentProps}>
<Item
onLoad={measure}
key={annotation.getId()}
{...childProps}
/>
</div>
</div>
)}
</CellMeasurer>
)
}
renderDynamicListOfItems(){
return (<div>
<List
height={500}
ref={listComponent => (_this.listComponent = listComponent)}
style={{
width: '100%'
}}
onRowsRendered={()=>{}}
width={1000}
rowCount={props.items.length}
rowHeight={this._cache.rowHeight}
rowRenderer={memoize(this.ItemView)}
// onScroll={onChildScroll}
className={listClassName}
overscanRowCount={0}
/>
</div>
)
}
Also, I am manually triggering the remeasurement of my item in its componentDidUpdate like follows()
Component Item
...
componentDidUpdate() {
console.log('loading called for ', this.props.annotation.getId())
this.props.onLoad()
}
...
In the main parent I am recomputing the heights of the list every time the list has updated and triggering a forceupdate as follows
Component ParentList
...
componentDidUpdate() {
console.log("calling this parent recomputing")
this.listComponent.recomputeRowHeights()
this.listComponent.forceUpdateGrid()
}
...
I just faced the same issue and it turned out to be some layout updates in my Item component, that changed the height of the item after it has been measured by the CellMeasurer.
Fix was to pass down the measure function and call it on every layout update.
In my case it was
images being loaded
and text being manipulated with Twemoji (which replaces the emojis with images).
I also had problems getting the correct height from the cache after prepending some new list items when scrolling to the top of the list. This could be fixed by providing a keyMapper to the CellMeasurerCache:
const [cache, setCache] = useState<CellMeasurerCache>();
useEffect(() => {
setCache(new CellMeasurerCache({
keyMapper: (rowIndex: number, columnIndex: number) => listItems[rowIndex].id,
defaultHeight: 100,
fixedWidth: true,
}));
}, [listItems]);

How to ensure state is not stale in React hook

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.

Categories

Resources