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.
Related
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.
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]);
I am pulling documents from Firebase, running calculations on them and separating the results into an array. I have an event listener in place to update the array with new data as it is populated.
I am using setTimeout to loop through an array which works perfectly with the initial data load, but occasionally, when the array is updated with new information, the setTimeout glitches and either begins looping through from the beginning rather than continuing the loop, or creates a visual issue where the loop doubles.
Everything lives inside of a useEffect to ensure that data changes are only mapped when the listener finds new data. I am wondering if I need to find a way to get the setTimeout outside of this effect? Is there something I'm missing to avoid this issue?
const TeamDetails = (props) => {
const [teamState, setTeamState] = useState(props.pushData)
const [slide, setSlide] = useState(0)
useEffect(() => {
setTeamState(props.pushData)
}, [props.pushData])
useEffect(()=> {
const teams = teamState.filter(x => x.regData.onTeam !== "null" && x.regData.onTeam !== undefined)
const listTeams = [...new Set(teams.map((x) => x.regData.onTeam).sort())];
const allTeamData = () => {
let array = []
listTeams.forEach((doc) => {
//ALL CALCULATIONS HAPPEN HERE
}
array.push(state)
})
return array
}
function SetData() {
var data = allTeamData()[slide];
//THIS FUNCTION BREAKS DOWN THE ARRAY INTO INDIVIDUAL HTML ELEMENTS
}
SetData()
setTimeout(() => {
if (slide === (allTeamData().length - 1)) {
setSlide(0);
}
if (slide !== (allTeamData().length - 1)) {
setSlide(slide + 1);
}
SetData();
console.log(slide)
}, 8000)
}, [teamState, slide]);
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
}
I have following problem, which I don't know how to properly tackle.
I have "list" of all purchased images on my view. I display them with v-for loop. Each image also has progress-bar element, so when user clicks on download button, downloadContent function gets executed and progress bar should be displayed.
So my html looks like this.
<section class="stripe">
<div class="stripe__item card" v-for="(i, index) in purchasedImages">
<progress-bar :val="i.download_progress"
v-if="i.download_progress > 0 && i.download_progress < 100"></progress-bar>
<div class="card__wrapper">
<img :src="'/'+i.thumb_path" class="card__img">
</div>
<div class="btn-img card__btn card__btn--left" #click="downloadContent(i.id_thumb, 'IMAGE', index)">
</div>
</div>
</section>
And this is my JS Code
import Vue from 'vue'
import orderService from '../api-services/order.service';
import downloadJs from 'downloadjs';
import ProgressBar from 'vue-simple-progress';
export default {
name: "MyLocations",
components: {
ProgressBar: ProgressBar
},
data() {
return {
purchasedImages: {},
purchasedImagesVisible: false,
}
},
methods: {
getUserPurchasedContent() {
orderService.getPurchasedContent()
.then((response) => {
if (response.status === 200) {
let data = response.data;
this.purchasedImages = data.images;
if (this.purchasedImages.length > 0) {
this.purchasedImagesVisible = true;
// Set download progress property
let self = this;
this.purchasedImages.forEach(function (value, key) {
self.purchasedImages[key].download_progress = 0;
});
}
}
})
},
downloadContent(id, type, index) {
let self = this;
orderService.downloadContent(id, type)
.then((response) => {
let download = downloadJs(response.data.link);
download.onprogress = function (e) {
if (e.lengthComputable) {
let percent = e.loaded / e.total * 100;
let percentage = Math.round(percent);
if (type === 'IMAGE') {
// Is this proper way to set one field reactive?
self.purchasedImages[index].download_progress = percentage;
if (percentage === 100) {
self.purchasedImages[index].download_progress = 0;
}
}
}
}
})
},
},
mounted: function () {
this.getUserPurchasedContent();
}
};
So the problem is. When user clicks on download button, download starts to execute and I get downloaded content, but I don't see progress bar. So I wonder, is this a proper way to set element reactive? How should my implementation look like? How to properly set self.purchasedImages[index].download_progress object key value, so progress bar will be visible?
If you need any additional informations, please let me know and I will provide. Thank you!
The snippet:
this.purchasedImages = data.images;
Leads us to believe data.images is an array of objects that do not have the download_progress property. So Vue can't detect/react when it changes.
To fix that, you can use Vue.set:
Vue.set(self.purchasedImages[key], 'download_progress', 0);
This is very well explained in Vue.js docs.
Another option: add the property before assigning to data
Just for completeness, you could also add the download_progress before assigning the array to the data property. This would allow Vue to notice it and be able to react to it.
Example:
let data = response.data;
this.purchasedImages = data.images.map(i => ({...i, download_progress: 0}));
if (this.purchasedImages.length > 0) {
this.purchasedImagesVisible = true;
// no need to set download_progress here as it was already set above
}
// if above could also be simplified to just:
this.purchasedImagesVisible = this.purchasedImages.length;
On a side note, since it is gonna be an array and not an object, I suggest you declare it as such:
data() {
return {
purchasedImages: [], // was: {},
This will have no effect, since you overwrite purchasedImages completely in (this.purchasedImages = data.images;), but it is a good practice as it documents that property's type.