can't use state value right after setState() - javascript

Currently i'm doing a quiz composed by multiple categories that can be chosen by the user and i wanna check if the user responded to all questions. For doing that, i compared the number of questions he answered with the number of questions gived by the api response. The problem is that i have an "submit answers" button at the end of the last question, with that onClick function:
const sendAnswers = (e, currentQuiz) => {
setQuizzes({...quizzes, [currentQuiz]:answers});
setAnswers([])
var answeredToAllQuestions = true
DataState.map(function (quiz) {
if(quiz.category in quizzes){
if(Object.keys(quiz.questions).length !== Object.keys(quizzes[quiz.category]).length){
answeredToAllQuestions=false;
}
}
});
if(answeredToAllQuestions === false){
setAlertTrigger(1);
}
else{
setNumber(number+1);
}
}
in that function i use setState on this line: setQuizzes({...quizzes, [currentQuiz]:answers}); to upload the answers he checked on the last question before checking if he answered to all questions. The problem is that state of quizzes is not updated imediatly and it s not seen by the if condition.
I really don't know how am i supposed to update the state right after setting it because, as i know, react useState updates the state at the next re-render and that causes trouble to me..

Considering that quizzes will be equal to {...quizzes, [currentQuiz]:answers} (after setQuizzes will set it), there is no reason to use quizzes in if condition. Replace it with a local var and problem will be solved.
const sendAnswers = (e, currentQuiz) => {
let futureValueOfQuizzes = {...quizzes, [currentQuiz]:answers}
setQuizzes(futureValueOfQuizzes);
setAnswers([])
var answeredToAllQuestions = true
DataState.map(function (quiz) {
if(quiz.category in futureValueOfQuizzes){
if(Object.keys(quiz.questions).length !== Object.keys(quizzes[quiz.category]).length){
answeredToAllQuestions=false;
}
}
});
if(answeredToAllQuestions === false){
setAlertTrigger(1);
}
else{
setNumber(number+1);
}
}
I would like to take this opportunity to say that these type of problems appear when you use React state for your BI logic. Don't do that! Much better use a local var defined in components body:
const Component = () => {
const [myVar , setMyVar] = useState();
let myVar = 0;
...
}
If myVar is used only for BI logic, use the second initialization, never the first!
Of course sometimes you need a var that is in BI logic and in render (so the state is the only way). In that case set the state properly but for script logic use a local var.

You have to either combine the useState hook with the useEffect or update your sendAnswers method to perform your control flow through an intermediary variable:
Using a temporary variable where next state is stored:
const sendAnswers = (e, currentQuiz) => {
const newQuizzes = {...quizzes, [currentQuiz]:answers};
let answeredToAllQuestions = true
DataState.map(function (quiz) {
if(quiz.category in newQuizzes){
if (Object.keys(quiz.questions).length !== Object.keys(newQuizzes[quiz.category]).length){
answeredToAllQuestions = false;
}
}
});
setQuizzes(newQuizzes);
setAnswers([]);
if (answeredToAllQuestions === false) {
setAlertTrigger(1);
} else {
setNumber(number+1);
}
}
Using the useEffect hook:
const sendAnswers = (e, currentQuiz) => {
setQuizzes({...quizzes, [currentQuiz]:answers});
setAnswers([]);
}
useEffect(() => {
let answeredToAllQuestions = true
DataState.map(function (quiz) {
if(quiz.category in quizzes){
if (Object.keys(quiz.questions).length !== Object.keys(quizzes[quiz.category]).length){
answeredToAllQuestions = false;
}
}
});
if (answeredToAllQuestions === false) {
setAlertTrigger(1);
} else {
setNumber(number+1);
}
}, [quizzes]);

Related

Stop setInterval function set inside useEffect

I have this simple useEffect code. When the user logged in to the application every 2 minutes I will dispatch an action which is an API call, and I need to stop this interval once a user is logged out. Still, the current code even runs after the user is logged out, what shall I do to prevent this interval when the user logs out.
I am using the value from the localStorage to determine whether the user is logged in or not.
const intervalId = useRef(null)
useEffect(() => {
const isLoggedIn = localStorage.getItem("isUserLoggedIn") //(true or false)
intervalId.current = setInterval(() => {
dispatch(refreshUserToken());
if(isLoggedIn === false){
clearInterval(intervalId.current)
}
},1000*60*2)
return () => {
clearInterval(intervalId.current)
}
},[])
Is there any way to resolve my issue?
Any help would be much appreciated!!
You should be adding the line where you get that value from localStorage inside the interval, if you want the updated value. Also, localStorage would gives you a string instead of a boolean, either you parse it, or you change your if statement. Try with this:
const intervalId = useRef(null);
useEffect(() => {
intervalId.current = setInterval(() => {
const isLoggedIn = localStorage.getItem("isUserLoggedIn"); //(true or false)
if (isLoggedIn === "false") {
clearInterval(intervalId.current);
return;
}
dispatch(refreshUserToken());
}, 1000 * 60 * 2);
return () => {
clearInterval(intervalId.current);
};
}, []);
You could use an event instead of a setInterval. As an example, change the code where you are setting the localStorage to this:
localStorage.setItem("isUserLoggedIn", true); // or false depending on the context
window.dispatchEvent(new Event("storage")); // you notice that there is a change
You change your useEffect to this:
useEffect(()=>{
const listenStorageChange = () => {
const isLoggedIn = localStorage.getItem("isUserLoggedIn");
console.log(isLoggedIn);
// do your logic here
};
window.addEventListener("storage", listenStorageChange);
return () => window.removeEventListener("storage", listenStorageChange);
},[])
The keys and the values stored with localStorage are always in the UTF-16 string format. As with objects, integer keys and booleans are automatically converted to strings.
So you have to call like this:
if(isLoggedIn === 'false'){
clearInterval(intervalId.current)
}
Check the documentation.

why my React setstate is not updating immediately?

This a my function
onSelectAction = (x, o) => {
var { takeActionsOptions } = this.props.main
console.log(o, "onSelectAction")
var tempAction=_.cloneDeep(takeActionsOptions)
_.keys(tempAction).map(a => {
if(a === x.processCode) {
tempAction[a].map((b)=>{
b.isSelected = b.id === o.id
})
}
else{
tempAction[a].map((b)=>{
b.isSelected = b.id === 0
})
}
})
StoreActions.setState({ takeActionsOptions:tempAction});
this.onClickTakeAction(o, x)
}
where tempAction is changing the property like the i wanted to. But when i m trying update the store... this { takeActionsOptions:tempAction} is not getting updated for the first time. After 2-3 clicks on the desired location this is getting updated. i want to update immediately in the store because there is another function which fetches data from the store and does another operation.
this is my other function which is using the take "takeActionsOptions " from store. so if that function is not updating then this function isnt working properly
onClickTakeAction = (o, x) => {
var { takeActionsOptions=[] } = this.props.main
var selectedAction = takeActionsOptions[x.processCode].find(a => a.isSelected)
if (selectedAction.id === 0) {
hydro.msg.info("Please select an option.")
return;
}
var tempAction=_.cloneDeep(takeActionsOptions)
_.keys(tempAction).map(a => {
tempAction[a].map((b)=>{
b.isSelected = b.id === 0
})
})
this.setState({takeActionsOptions:tempAction})
switch (selectedAction.id) {
case 1:
var userName = somecode.userName;
if (userName.toUpperCase() === x.userName.toUpperCase()) {
Actions.deleteSelectedProcess(x);
}
else {
somecode.info("Not your Process")
}
break;
case 2:
Action.downloadLogs(x);
break;
}
}
var tempAction=_.cloneDeep(takeActionsOptions)
What the cloneDeep function is doing here? If it does any API calling/Getting data from the server, you need to wait for a moment to get the data. Meanwhile, you can disable the button and show some loaders for interactivity.
If you're using the loadash to deep copy the object, up to my knowledge loadash functions, takes a long time to complete based on the CPU or object you are trying to copy. So try to wait for a minute and check whether it's updating or not. If it is updating, then you should disable the button until then.

Generating x number of React components from integer variable

So I have a webpage that's meant to model a sort of questionnaire. Each question would take up the whole page, and the user would switch back and forth between questions using the arrow keys. That part's fine, swapping components on button pressing doesn't seem to be an issue, and I got a proof-of-concept of that working before.
The trouble is, these questionnaires won't have the same number of questions every time. That'll depend on the choice of the person making the questionnaire. So I stored the number of questions in a variable held in the Django Model, and I fetch that variable and try to generate x number of components based on that integer. At the moment I'm just trying to get it to generate the right number of components and let me navigate between them properly, I'll worry about filling them with the proper content later, because I'm already having enough trouble with just this. So here's my code:
import React, { useState, useEffect, cloneElement } from 'react';
import { useParams } from 'react-router-dom'
import QuestionTest from './questiontest';
function showNextStage(displayedTable, questionCount) {
let newStage = displayedTable + 1;
if (newStage > questionCount) {
newStage = questionCount;
}
return newStage;
}
function showPreviousStage(displayedTable) {
let newStage = displayedTable - 1;
if (newStage < 1) {
newStage = 1;
}
return newStage;
}
export default function Questionnaire(props) {
const initialState = {
questionCount: 2,
is_host: false
}
const [ roomData, setRoomData ] = useState(initialState)
const { roomCode } = useParams()
useEffect(() => {
fetch("/audio/get-room" + "?code=" + roomCode)
.then(res => res.json())
.then(data => {
setRoomData({
roomData,
questionCount: data.questionCount,
isHost: data.isHost,
})
})
},[roomCode,setRoomData])
const [ displayedTable, setDisplayedTable ] = useState(1);
let initialComponents = {};
for (let i = 1; i < roomData.questionCount + 1; i++) {
initialComponents[i] = <div><p>{i}</p> <QuestionTest /></div>
}
// "<QuestionTest />" is just a random component I made and "{i}" is
// to see if I'm on the right one as I test.
const [components] = useState(initialComponents);
useEffect(() => {
window.addEventListener('keydown', (e) => {
if (e.keyCode == '39') {
setDisplayedTable(showNextStage(displayedTable, roomData.questionCount));
} else if (e.keyCode == '37') {
setDisplayedTable(showPreviousStage(displayedTable));
}
});
return () => {
window.removeEventListener('keydown', (e) => {
if (e.keyCode == '39') {
setDisplayedTable(showNextStage(displayedTable, roomData.questionCount));
} else if (e.keyCode == '37') {
setDisplayedTable(showPreviousStage(displayedTable));
}
});
};
}, []);
return (
<div>
{components[displayedTable]}
</div>
)
}
So the trouble with this is, I need to set an initial state for the questionCount variable, or else I get errors. But this initial state is replaced almost immediately with the value set for this questionnaire, as retrieved by the fetch function, and so the state resets. The initial questionCount value of 2 however gets used in the component generation and in the adding of the eventListeners, and so when the page is generated it just has two components rather than a number matching questionCount's value for the questionnaire.
I don't really understand this tbh. If I remove the window.addEventLister from useEffect and make it standalone, then it works and the right number of components are generated, but then it adds a new EventListener every time the state refreshes, which starts to cause immense lag as you switch back and forth between pages as the function calls pile up and up.
So I don't really know how to achieve this. My entire way of going about this from the onset is probably terribly wrong (I just included it to show I made an attempt and wasn't just asking for it to be done for me), but I can't find any examples of what I'm trying to do online.

React Child Component Is Not Rerendering When Props Are Updated

My parent component takes input from a form and the state changes when the value goes out of focus via onBlur.
useEffect(() => {
let duplicate = false;
const findHierarchy = () => {
duplicationSearchParam
.filter(
(object, index) =>
index ===
duplicationSearchParam.findIndex(
(obj) => JSON.stringify(obj.name) === JSON.stringify(object.name)
)
)
.map((element) => {
DuplicateChecker(element.name).then((data) => {
if (data.status > 200) {
element.hierarchy = [];
} else {
element.hierarchy = data;
}
});
if (duplicate) {
} else {
duplicate = element?.hierarchy?.length !== 0;
}
});
return duplicate;
};
let dupe = findHierarchy();
if (dupe) {
setConfirmationProps({
retrievedData: formData,
duplicate: true,
responseHierarchy: [...duplicationSearchParam],
});
} else {
setConfirmationProps({
retrievedData: formData,
duplicate: false,
responseHierarchy: [],
});
}
}, [duplicationSearchParam]);
I have a child component also uses a useeffect hook to check for any state changes of the confirmationProps prop.
the issue is that the event gets triggered onblur, and if the user clicks on the next button. this function gets processes
const next = (data) => {
if (inProgress === true) {
return;
}
inProgress = true;
let countryLabels = [];
formData.addresses?.map((address) => {
fetch(`/api/ref/country/${address?.country}`)
.then((data) => {
countryLabels.push(data.label);
return countryLabels;
})
.then((countries) => {
let clean = MapCleanse(data, countries);
fetch("/api/v1/organization/cleanse", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(clean),
})
.then((data) => {
if (data.status > 200) {
console.log(data.message);
message.error(getErrorCode(data.message.toString()));
} else {
Promise.all([confirmationProps, duplicationSearchParam]).then(
(values) => {
console.log(values);
console.log(data);
setCleansed(data);
**setCurrent(current + 1);**
inProgress = false;
}
);
}
})
.catch((err) => {
console.log(err);
inProgress = false;
});
})
.catch((err) => {
console.log(err);
inProgress = false;
});
});
console.log(confirmationProps);
};
The important part in the above code snippet is the setCurrent(current + 1) as this is what directs our code to render the child component
in the child component, i have a use effect hook that is watching [props.duplicateData.responseHierarchy]
I do output the values of props.duplicateData.responsehierarchy to the console to see if the updated information gets passed to the child component and it does. the values are present.
I have a conditional render statement that looks like this
{cleansedTree?.length > 0 || treeDuplicate ? (...)}
so although the data is present and is processed and massaged in the child component. it still will not re render or display properly. unless the user goes back to the previous screen and proceeds to the next screen again... which forces a re-render of the child component.
I have boiled it down and am assuming that the conditional rendering of the HTML is to blame. Or maybe when the promise resolves and the state gets set for the confirmation props that the data somehow gets lost or the useefect doesn't pick it up.
I have tried the useefect dependency array to contain the props object itself and other properties that arent directly related
UPDATE: this is a code snippet of the processing that gets done in the childs useeffect
useEffect(() => {
console.log(props.duplicate);
console.log(props.duplicateData);
console.log(props.confirmationProps);
let newArray = props.duplicateData.filter((value) => value);
let duplicateCheck = newArray.map((checker) =>
checker?.hierarchy?.find((Qstring) =>
Qstring?.highlightedId?.includes(UUIDToString(props?.rawEdit?.id))
)
);
duplicateCheck = duplicateCheck.filter((value) => value);
console.log(newArray, "new array");
console.log(duplicateCheck, "duplicate check");
if (newArray?.length > 0 && duplicateCheck?.length === 0) {
let list = [];
newArray.map((dupeData) => {
if (dupeData !== []) {
let clean = dupeData.hierarchy?.filter(
(hierarchy) => !hierarchy.queryString
);
let queryParam = dupeData.hierarchy?.filter(
(hierarchy) => hierarchy.queryString
);
setSelectedKeys([queryParam?.[0]?.highlightedId]);
let treeNode = {};
if (clean?.length > 0) {
console.log("clean", clean);
Object.keys(clean).map(function (key) {
treeNode = buildDuplicate(clean[key]);
list.push(treeNode);
return list;
});
setCleansedTree([...list]);
setTreeDuplicate(true);
} else {
setTreeDuplicate(false);
}
}
});
}
}, [props.duplicateData.responseHierarchy]);
This is a decently complex bit of code to noodle through, but you did say that **setCurrent(current + 1);** is quite important. This pattern isn't effectively handling state the way you think it is...
setCurrent(prevCurrent => prevCurrent + 1)
if you did this
(count === 3)
setCount(count + 1) 4
setCount(count + 1) 4
setCount(count + 1) 4
You'd think you'd be manipulating count 3 times, but you wouldn't.
Not saying this is your answer, but this is a quick test to see if anything changes.
The issue with this problem was that the state was getting set before the promise was resolved. to solve this issue I added a promise.all function inside of my map and then proceeded to set the state.
What was confusing me was that in the console it was displaying the data as it should. but in fact, as I learned, the console isn't as reliable as you think. if someone runs into a similar issue make sure to console the object by getting the keys. this will return the true state of the object, and solve a lot of headache

Tracking changes in state?

The component code has several parameters, each of which has an initial value received from the server. How can I track that one of them (or several at once) has changed its state from the original one in order to suggest that the user save the changes or reset them?
Something similar can be seen in Discord when changing the profile / server.
The solution I found using useEffect () looks redundant, because there may be many times more options.
const [hiddenData, setHiddenData] = useState(server.hidden_data);
const [hiddenProfile, setHiddenProfile] = useState(server.hidden_profile);
const [isChanged, setIsChanged] = useState(false);
useEffect(() => {
if (hiddenData !== server.hidden_data
|| hiddenProfile !== server.hidden_profile) {
setIsChanged(true);
} else {
setIsChanged(false);
}
}, [hiddenData, server.hidden_data, hiddenProfile, server.hidden_profile]);
return (
<>
{isChanged && <div>You have unsaved changes!</div>}
</>
);
Maybe something like that?
const [draftState, setDraftState] = useState(server)
const [state, setState] = useState(server)
// a more complex object with the list of changed props is fine too
const isChanged = lodash.isEqual(state, draftState)
function changeProp (prop, value) {
setState({
...draftState,
[prop]: value
})
}
function saveState () {
setState(draftState)
// Persist state if needed
}

Categories