React screen update from a loop process - reactjs

I'm creating a React app that reads data from a text file and I want to display the processing progress.
I perform multiple data transformations in sequence (as they are dependent on each other) and I want to have a progress bar for each of the processing.
I have a progress component that works as expected but I'm not able to update the progress whilst it's happening, all bars are fully filled after the whole process finishes.
From what I can see, React is queueing all state updates and rendering everything in the end.
So, this is what I have at the moment:
Analysis.js
export function Analysis ({ onError, whatsapp }) {
const [baseContent, setBaseContent] = useState(0)
const [chatContent, setChatContent] = useState(0)
const [splitMessages, setSplitMessages] = useState(0)
function onSetBaseContent (value) {
setBaseContent(value || baseContent + 1)
}
function onSetChatContent (value) {
setChatContent(value || chatContent + 1)
}
function onSplitMessages (value) {
setSplitMessages(value || splitMessages + 1)
}
const whatsappCallBacks = { onSetBaseContent, onSetChatContent, onSplitMessages }
useEffect(() => {
whatsapp.setData(whatsappCallBacks)
}, [])
return (
<>
<Row>
<p>
Analysing...
</p>
</Row>
<Row>
<Col xs={1}></Col>
<Col>
<Progress title = 'Base content' percentage={baseContent}></Progress>
<Progress title = 'Chat content' percentage={chatContent}></Progress>
<Progress title = 'Content split' percentage={splitMessages}></Progress>
</Col>
<Col xs={1}></Col>
</Row>
</>
)
}
whatsapp is an instance of the Whatsapp class created before.
The Progress component simply has a bar that goes up to 100% width, and it's tested and working.
Here is the important part of the Whatsapp class
Whatsapp.js
...
async setData ({ onSetBaseContent, onSetChatContent, onSplitMessages }) {
this.setBaseContent(onSetBaseContent)
this.setMessages(onSetChatContent, onSplitMessages)
}
setBaseContent (onSetBaseContent) {
this.content = this.file.content.replace(/\r\n/, '\n').replace(/\r/, '\n').split('\n')
onSetBaseContent(100)
}
setMessages (onSetChatContent, onSplitMessages) {
// Read each line and put them as a string entry. If the line does not match
// the messageRegEx, includes in the previus entry with a line break
let percentage = Number((this.content.length / 100).toFixed())
for (let i = 0; i < this.content.length; i++) {
if (i % percentage === 0) {
onSetChatContent()
}
const message = this.content[i]
if (message.match(this.messageRegEx)) {
this.messages.push(message)
} else {
// Join the lines that are continuation of the previus message
this.messages[i - 1] += `\n${message}`
}
}
onSetChatContent(100)
const contact = new Contact()
const replacementsFileContent = []
this.setDateFormat()
percentage = Number((this.messages.length / 100).toFixed())
let i = 0
// Replace each entry by the Message instance and remove the null entries
this.messages = this.messages
.map(m => {
if (i % percentage === 0) {
onSplitMessages()
}
i++
const split = this.contentSplitRegex.exec(m)
if (!split) {
return null
}
const date = this.dateFormat === dateFormat.DAY_MONTH_YEAR
? `${split[1]}/${split[2]}/${split[3]} ${split[4]}:${split[5]}:${split[6]}`
: `${split[2]}/${split[1]}/${split[3]} ${split[4]}:${split[5]}:${split[6]}`
let cont = Contact.clean(split[7])
const content = split[8]
const replaced = contact.replace(cont)
if (!replaced && !replacementsFileContent.find(r => r === cont)) {
replacementsFileContent.push(cont)
} else if (replaced) {
cont = replaced
}
return new Message(date, cont, content)
})
.filter(messsage => messsage != null)
onSplitMessages(100)
if (replacementsFileContent.length > 0) {
Contact.saveReplacements(replacementsFileContent)
}
if (this.messages.length === 0) {
throw new Error('Failed to read messages from the file')
}
}
Debugging the code I can see the state updates are called correctly, but the progress is only updated (up to 100) all at the same time after the whole processing finishes.
Here's a video I recorded with the result
I already tried some "techniques" to force React to update: setTimeout, setInterval, updateState, moving things inside useEffect and nothing so far worked as they should.
I'm about to give up on react and do it using vanilla JS.
Source code here.

Related

Sorting utility function not working as intended

So I'm creating a web app using react with typescript that calls an api endpoint (it's a yugioh card database https://db.ygoprodeck.com/api-guide/). I should probably also point out that I'm using RTK Query, it might be a caching things I don't know. Specifically for my problem, it's a query that makes a fuzzy name search to see if a user's input text matches any card in the database. The returned array from the api however is not sorted in a way that I desire, so I wanted to fix that with a utility function as I can see myself sorting this way in multiple places.
Given the search "Dark" for example, the api returns all cards where dark appears in the name, regardless of when in the name the word appear. A card could for example be called "Angel of Dark" and it would come before a much more likely search "Dark Magician" because the api returns it in alphabetical order. But I want it to be sorted in a way that cards that starts with the word appears before cards where the word appears later.
I think I've done it in a way that should return it correctly, but alas it doesn't, so I figured the experts could maybe tell me where the code is wrong, if it's in the utility function or if it's with RTKQ or somewhere else where I'm just completely wrong.
Here's the utility function:
import { CardData } from "../models/cardData.interface";
const SortNameBySearch: Function = (search: string, cardData: CardData) => {
const sortedData = [...cardData.data];
sortedData.sort((a, b) => {
// Sort results by matching name with search position in name
if (
a.name.toLowerCase().indexOf(search.toLowerCase()) >
b.name.toLowerCase().indexOf(search.toLowerCase())
) {
return 1;
} else if (
a.name.toLowerCase().indexOf(search.toLowerCase()) <
b.name.toLowerCase().indexOf(search.toLowerCase())
) {
return -1;
} else {
if (a.name > b.name) return 1;
else return -1;
}
});
return sortedData;
};
export default SortNameBySearch;
I've sliced the array to only show the top 3 results and it looks like this, but it's weird because the first one should't even be in this array as it doesn't contain the searched name, which makes me think it might be a caching thing with RTKQ but I'm honestly clueless as how to fix that.
For good measure and better context here's how I'm using it in context
import { FC, useEffect, useRef, useState } from "react";
import { useGetCardsBySearchQuery } from "../../../../services/api";
import { SearchResultsProps } from "./SearchResults.types";
import SortNameBySearch from "../../../../utils/SortNamesBySearch";
import { Card } from "../../../../models/card.interface";
const SearchResults: FC<SearchResultsProps> = ({ search }) => {
const [searched, setSearched] = useState("");
const [typing, setTyping] = useState(false);
const searchRef = useRef(0);
useEffect(() => {
clearTimeout(searchRef.current);
setTyping(true);
if (search !== null) {
searchRef.current = window.setTimeout(() => {
setSearched(search);
setTyping(false);
}, 100);
}
}, [search]);
const { data, isLoading, isError, isSuccess } = useGetCardsBySearchQuery(
searched,
{ skip: typing }
);
const results =
!typing && isSuccess
? SortNameBySearch(searched, data)
.slice(0, 3)
.map((card: Card) => {
return (
<p key={card.id} className="text-white">
{card.name}
</p>
);
})
: null;
if (isError) {
return <div>No card matching your query was found in the database.</div>;
}
return (
<>
{isLoading ? (
"loading..."
) : typing ? (
"loading..."
) : (
<div className="bg-black relative">{results}</div>
)}
</>
);
};
export default SearchResults;
Given that I'm still new to this whole thing there are likely other issues with my code and of course I want to improve, so if there's something in there that just doesn't make sense please do let me know.
Looking forward to see your answers.
-Benjamin
Here is your code changed to do as you said in the last comment.
Not fully tested, but it should be something like this, so I guess this should be enough to get you going.
import { CardData } from "../models/cardData.interface";
const SortNameBySearch: Function = (search: string, cardData: CardData) => {
const sortedData = [...cardData.data];
const dataWithSearchAtIndex0: string[] = [];
const dataWithSearchAtOtherIndexes: string[] = [];
for(const data of sortedData) {
const searchIndex = data.toLowerCase().indexOf(search.toLowerCase());
if (searchIndex === 0) {
dataWithSearchAtIndex0.push(data);
} else if (searchIndex > 0) {
dataWithSearchAtOtherIndexes.push(data);
}
}
dataWithSearchAtIndex0.sort((a, b) => sortIgnoringCase(a, b));
dataWithSearchAtOtherIndexes.sort((a, b) => sortIgnoringCase(a, b));
return [...dataWithSearchAtIndex0, ...dataWithSearchAtOtherIndexes];
}
const sortIgnoringCase: Function = (a: string, b: string) => {
const lowerCaseA = a.toLowerCase(); // ignore upper and lowercase
const lowerCaseB = b.toLowerCase(); // ignore upper and lowercase
if (lowerCaseA < lowerCaseB) {
return -1;
}
if (lowerCaseA > lowerCaseB) {
return 1;
}
// a and b must be equal
return 0;
};
export default SortNameBySearch;

Why doesn't useRef trigger a child rerender in this situation?

I'm running into an issue where my hobbies list is not being displayed if it was previously undefined.
The goal is to cycle through a list of hobbies and display them in the UI. If one hobby is passed, only one should be displayed. If none are passed, nothing should be displayed.
Basically, setHobbies(["Cycling"]) followed by setHobbies(undefined) correctly turns off the hobbies render, but then the next setHobbies(["Reading"]) doesn't show up.
Using a debugger I've been able to verify that the relevant code in the useEffect hook in EmployeeInfoWrapper does get triggered, and hobbiesRef.current set accordingly, but it doesn't cause EmployeeInfo to rerender.
Container:
const Container = () => {
const [hobbies, setHobbies] = React.useState<string[]>();
setLabels(["Board games"]); // example of how it's set; in reality this hook is passed down and set in lower components
return (
<SpinnerWrapper hobbies=hobbies otherInfo=otherInfo />
);
};
EmployeeInfoWrapper:
export const EmployeeInfoWrapper = (props) => {
const { hobbies, otherInfo } = props;
const [indx, setIndx] = React.useState<number>(0);
// this doesn't work due to https://github.com/facebook/react/issues/14490, shouldn't matter though because it's handled in the useEffect
const hobbyRef = React.useRef(hobbies?.length ? hobbies[indx] : "");
useEffect(() => {
if (hobbies?.length > 1) { // doubt this is relevant as my bug is with 0 or 1-length hobbies prop
hobbyRef.current = hobbies[indx];
setTimeout(() => setIndx((indx + 1) % hobbies.length), 2000);
}
if (hobbies?.length == 1) {
hobbyRef.current = hobbies[indx]; // can see with debugger this line is hit
}
if (!hobbies?.length) { // covers undefined or empty cases
hobbyRef.current = "";
}
}, [hobbies]);
return (
<EmployeeInfo hobby={hobbyRef.current} otherInfo={otherInfo} />
);
};

Takes two clicks for react bootstrap popover to show up

I've run into an issue while trying to build a page that allows the user to click on a word and get its definition in a bootstrap popover. That is achieved by sending an API request and updating the state with the received data.
The problem is that the popover only appears after the second click on the word. The console.log() in useEffect() shows that every time a new word is clicked an API request is made. For the popover to appear the same word must be clicked twice. It'd be better if it only took one click.
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { Alert, Popover, OverlayTrigger } from "react-bootstrap";
export default function App() {
const [text, setText] = useState(
"He looked at her and saw her eyes luminous with pity."
);
const [selectedWord, setSelectedWord] = useState("luminous");
const [apiData, setApiData] = useState([
{
word: "",
phonetics: [{ text: "" }],
meanings: [{ definitions: [{ definition: "", example: "" }] }]
}
]);
const words = text.split(/ /g);
useEffect(() => {
var url = "https://api.dictionaryapi.dev/api/v2/entries/en/" + selectedWord;
axios
.get(url)
.then(response => {
setApiData(response.data)
console.log("api call")
})
.catch(function (error) {
if (error) {
console.log("Error", error.message);
}
});
}, [selectedWord]);
function clickCallback(w) {
var word = w.split(/[.!?,]/g)[0];
setSelectedWord(word);
}
const popover = (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData[0].word}</h1>
<h6>{apiData[0].meanings[0].definitions[0].definition}</h6>
</Popover.Body>
</Popover>
);
return (
<Alert>
{words.map((w) => (
<OverlayTrigger
key={uuid()}
trigger="click"
placement="bottom"
overlay={popover}
>
<span onClick={() => clickCallback(w)}> {w}</span>
</OverlayTrigger>
))}
</Alert>
);
}
UPDATE:
Changed the apiData initialization and the <Popover.Body> component. That hasn't fixed the problem.
const [apiData, setApiData] = useState(null)
<Popover.Body>
{
apiData ?
<div>
<h1>{apiData[0].word}</h1>
<h6>{apiData[0].meanings[0].definitions[0].definition}</h6>
</div> :
<div>Loading...</div>
}
</Popover.Body>
The Problem
Here's what I think is happening:
Component renders
Start fetching definition for "luminous".
The definition of "luminous" has finished being fetched. It calls setApiData(data).
Component rerenders
If you click "luminous", the popper is shown immediately, this is because the data for the popper is ready to use and setSelectedWord("luminous") does nothing.
If you click another word, such as "pity", the popper attempts to show, but setSelectedWord("pity") causes the component to start rerendering.
Component rerenders
Start fetching definition for "pity".
The definition of "pity" has finished being fetched. It calls setApiData(data).
Component rerenders
If you click "pity", the popper is shown immediately, this is because the data for the popper is ready to use and setSelectedWord("pity") does nothing.
Selecting another word will repeat this process over and over.
To fix this, you need to first make use of the show property to show the popover after rendering it out if it matches the selected word. But what if the word appears multiple times? If you did this for the word "her", it would show the popover in multiple places. So instead of comparing against each word, you'd have to assign each word a unique ID and compare against that.
Fixing the Component
To assign words an ID that won't change between renders, we need to assign them IDs at the top of your component and store them in an array. To make this "simpler", we can abstract that logic into a re-useable function outside of your component:
// Use this function snippet in demos only, use a more robust package
// https://gist.github.com/jed/982883 [DWTFYWTPL]
const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}
// Splits the text argument into words, removes excess formatting characters and assigns each word a UUID.
// Returns an array with the shape: { [index: number]: { word: string, original: string, uuid: string }, text: string }
function identifyWords(text) {
// split input text into words with unique Ids
const words = text
.split(/ +/)
.map(word => {
const cleanedWord = word
.replace(/^["]+/, "") // remove leading punctuation
.replace(/[.,!?"]+$/, "") // remove trailing punctuation
return { word: cleanedWord, original: word, uuid: uuid() }
});
// attach the source text to the array of words
// we can use this to prevent unnecessary rerenders
words.text = text;
// return the array-object
return words;
}
Within the component, we need to setup the state variables to hold the words array. By passing a callback to useState, React will only execute it on the first render and skip calling it on rerenders.
// set up state array of words that have their own UUIDs
// note: we don't want to call _setWords directly
const [words, _setWords] = useState(() => identifyWords("He looked at her and saw her eyes luminous with pity."));
Now that we have words and _setWords, we can pull out the text value from it:
// extract text from words array for convenience
// probably not needed
const text = words.text;
Next, we can create our own setText callback. This could be simpler, but I wanted to make sure we support React's mutating update syntax (setText(oldValue => newValue)):
// mimic a setText callback that actually updates words as needed
const setText = (newTextOrCallback) => {
if (typeof newTextOrCallback === "function") {
// React mutating callback mode
_setWords((words) => {
const newText = newTextOrCallback(words.text);
return newText === words.text
? words // unchanged
: identifyWords(newText); // new value
});
} else {
// New value mode
return newTextOrCallback === words.text
? words // unchanged
: identifyWords(newTextOrCallback); // new value
}
}
Next, we need to set up the currently selected word. Once the definition is available, this word's popover will be shown.
const [selectedWordObj, setSelectedWordObj] = useState(() => words.find(({word}) => word === "luminous"));
If you don't want to show a word by default, use:
const [selectedWordObj, setSelectedWordObj] = useState(); // nothing selected by default
To fix the API call, we need to make use of the "use async effect" pattern (there are libraries out there to simplify this):
const [apiData, setApiData] = useState({ status: "loading" });
useEffect(() => {
if (!selectedWordObj) return; // do nothing.
// TODO: check cache here
// clear out the previous definition
setApiData({ status: "loading" });
let unsubscribed = false;
axios
.get(`https://api.dictionaryapi.dev/api/v2/entries/en/${selectedWordObj.word}`)
.then(response => {
if (unsubscribed) return; // do nothing. out of date response
const body = response.data;
// unwrap relevant bits
setApiData({
status: "completed",
word: body.word,
definition: body.meanings[0].definitions[0].definition
});
})
.catch(error => {
if (unsubscribed) return; // do nothing. out of date response
console.error("Failed to get definition: ", error);
setApiData({
status: "error",
word: selectedWordObj.word,
error
});
});
return () => unsubscribed = true;
}, [selectedWord]);
The above code block makes sure to prevent calling the setApiData methods when they aren't needed any more. It also uses a status property to track it's progress so you can render the result properly.
Now to define a popover that shows a loading message:
const loadingPopover = (
<Popover id="popover-basic">
<Popover.Body>
<span>Loading...</span>
</Popover.Body>
</Popover>
);
We can mix that loading popover with apiData to get a popover to show the definition. If we're still loading the definition, use the loading one. If we've had an error, show the error. If it completed properly, render out the defintion. To make this easier, we can put this logic in a function outside of your component like so:
function getPopover(apiData, loadingPopover) {
switch (apiData.status) {
case "loading":
return loadingPopover;
case "error":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>Couldn't find definition for {apiData.word}: {apiData.error.message}</h6>
</Popover.Body>
</Popover>
);
case "completed":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>{apiData.definition}</h6>
</Popover.Body>
</Popover>
);
}
}
We call this funtion in the component using:
const selectedWordPopover = getPopover(apiData, loadingPopover);
Finally, we render out the words. Because we are rendering out an array, we need to use a key property that we'll set to each word's Id. We also need to select the word that was clicked - even if there were more than one of the same words, we only want just the clicked one. For that we'll check its Id too. If we click on a particular word, we need to sure that the one we clicked on is selected. We also need to render out the original word with its punctuation. This is all done in this block:
return (
<Alert>
{words.map((wordObj) => {
const isSelectedWord = selectedWordObj && selectedWordObj.uuid = wordObj.uuid;
return (
<OverlayTrigger
key={wordObj.uuid}
show={isSelectedWord}
trigger="click"
placement="bottom"
overlay={isSelectedWord ? selectedWordPopover : loadingPopover}
>
<span onClick={() => setSelectedWordObj(wordObj)}> {wordObj.original}</span>
</OverlayTrigger>
)})}
</Alert>
);
Complete Code
Bringing all that together gives:
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { Alert, Popover, OverlayTrigger } from "react-bootstrap";
// Use this function snippet in demos only, use a more robust package
// https://gist.github.com/jed/982883 [DWTFYWTPL]
const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}
// Splits the text argument into words, removes excess formatting characters and assigns each word a UUID.
// Returns an array with the shape: { [index: number]: { word: string, original: string, uuid: string }, text: string }
function identifyWords(text) {
// split input text into words with unique Ids
const words = text
.split(/ +/)
.map(word => {
const cleanedWord = word
.replace(/^["]+/, "") // remove leading characters
.replace(/[.,!?"]+$/, "") // remove trailing characters
return { word: cleanedWord, original: word, uuid: uuid() }
});
// attach the source text to the array of words
words.text = text;
// return the array
return words;
}
function getPopover(apiData, loadingPopover) {
switch (apiData.status) {
case "loading":
return loadingPopover;
case "error":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>Couldn't find definition for {apiData.word}: {apiData.error.message}</h6>
</Popover.Body>
</Popover>
);
case "completed":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>{apiData.definition}</h6>
</Popover.Body>
</Popover>
);
}
}
export default function App() {
// set up state array of words that have their own UUIDs
// note: don't call _setWords directly
const [words, _setWords] = useState(() => identifyWords("He looked at her and saw her eyes luminous with pity."));
// extract text from words array for convenience
const text = words.text;
// mimic a setText callback that actually updates words as needed
const setText = (newTextOrCallback) => {
if (typeof newTextOrCallback === "function") {
// React mutating callback mode
_setWords((words) => {
const newText = newTextOrCallback(words.text);
return newText === words.text
? words // unchanged
: identifyWords(newText); // new value
});
} else {
// New value mode
return newTextOrCallback === words.text
? words // unchanged
: identifyWords(newTextOrCallback); // new value
}
}
const [selectedWordObj, setSelectedWordObj] = useState(() => words.find(({word}) => word === "luminous"));
const [apiData, setApiData] = useState({ status: "loading" });
useEffect(() => {
if (!selectedWordObj) return; // do nothing.
// TODO: check cache here
// clear out the previous definition
setApiData({ status: "loading" });
let unsubscribed = false;
axios
.get(`https://api.dictionaryapi.dev/api/v2/entries/en/${selectedWordObj.word}`)
.then(response => {
if (unsubscribed) return; // do nothing. out of date response
const body = response.data;
// unwrap relevant bits
setApiData({
status: "completed",
word: body.word,
definition: body.meanings[0].definitions[0].definition
});
})
.catch(error => {
if (unsubscribed) return; // do nothing. out of date response
console.error("Failed to get definition: ", error);
setApiData({
status: "error",
word: selectedWordObj.word,
error
});
});
return () => unsubscribed = true;
}, [selectedWord]);
function clickCallback(w) {
var word = w.split(/[.!?,]/g)[0];
setSelectedWord(word);
}
const loadingPopover = (
<Popover id="popover-basic">
<Popover.Body>
<span>Loading...</span>
</Popover.Body>
</Popover>
);
const selectedWordPopover = getPopover(apiData, loadingPopover);
return (
<Alert>
{words.map((wordObj) => {
const isSelectedWord = selectedWordObj && selectedWordObj.uuid = wordObj.uuid;
return (
<OverlayTrigger
key={wordObj.uuid}
show={isSelectedWord}
trigger="click"
placement="bottom"
overlay={isSelectedWord ? selectedWordPopover : loadingPopover}
>
<span onClick={() => setSelectedWordObj(wordObj)}> {wordObj.original}</span>
</OverlayTrigger>
)})}
</Alert>
);
}
Note: You can improve this by caching the results from the API call.

Managing React states correctly giving strange error

I'm trying to make a hangman game using React.
Here is the code: https://pastebin.com/U1wUJ28G
When I click on a letter I get error:
Here's AvailableLetter.js:
import React, {useState} from 'react';
import classes from './AvailableLetter.module.css';
const AvailableLetter = (props) => {
const [show,setShow]=useState(true);
// const [clicked, setClicked]=useState(false);
// const [outcome,setOutcome]=useState(false);
// if (show)
// {
// setClicked(true);
// }
const play = (alphabet) => {
const solution = props.solution.split('');
if (solution.indexOf(alphabet)<0)
{
return false;
}
else
{
return true;
}
}
if (!show)
{
if (play(props.alphabet))
{
props.correct();
// alert('correct');
}
else
{
props.incorrect(); // THIS CAUSES ERROR!!!!
// alert('wrong');
}
}
return (
show ? <span show={show} onClick={()=>{setShow(false)}} className={classes.AvailableLetter}>{props.alphabet}</span> : null
);
}
export default AvailableLetter;
I suspect the error is caused by not managing state properly inside AvailableLetter.js. But I don't know why the error is showing pointing to Hangman.js.
Here's what's pointed to by props.incorrect:
Game.js:
guessedIncorrectHandler = (letter) => {
const index = this.state.availableLetters.indexOf(letter);
let newAvailableLetters = [...this.state.availableLetters];
newAvailableLetters.splice(index,1);
let newUsedLetters = [...this.state.usedLetters];
newUsedLetters.push(letter);
const oldValueLives = this.state.lives;
const newValueLives = oldValueLives - 1;
this.setState({
usedLetters: newUsedLetters,
availableLetters: newAvailableLetters,
lives: newValueLives
});
};
Applied fix kind user suggested on
lives: newValueLives < 0 ? 0 : newValueLives
But now when I lick on a letter I get multiple letters get added randomly to correct letters and incorrect letters on a single click.
If I interrupt guessedIncorrectHandler with return true:
guessedIncorrectHandler = (letter) => {
const index = this.state.availableLetters.indexOf(letter);
let newAvailableLetters = [...this.state.availableLetters];
newAvailableLetters.splice(index,1);
let newUsedLetters = [...this.state.usedLetters];
newUsedLetters.push(letter);
const oldValueLives = this.state.lives;
const newValueLives = oldValueLives - 1;
console.log('[newValueLives] ',newValueLives); return true; // HERE INTERRUPTED
this.setState({
usedLetters: newUsedLetters,
availableLetters: newAvailableLetters,
lives: newValueLives < 0 ? 0 : newValueLives
});
};
Now when I click 'a' for example (which is correct since solution is 'apple') it behaves correctly.
When app is running I get warning:
Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
in AvailableLetter (at Letters.js:8)
...
Probably the cause for this warning is inside AvailableLetter.js inside return:
show ? <span show={show} onClick={()=>{setShow(false)}} className={classes.AvailableLetter}>{props.alphabet}</span> : null
Tried to set up codesandbox here: https://codesandbox.io/s/fast-breeze-o633v
The issue can occur if the lives value is a negative number.
Try to secure that possibility:
this.setState({
usedLetters: newUsedLetters,
availableLetters: newAvailableLetters,
lives: newValueLives < 0 ? 0 : nevValueLives, // if lives is negative, assign 0
});

Leafet setLatLng unmounts and mounts markers continuously, preventing events from firing

I'm trying to visualise 500+ vehicles using leaflet. When the position of a marker (vehicle) changes, it will move slowly to reach the destination (using requestAnimationFrame and leaflet's 'native' setLatLng since I don't want to update the state directly). It works well, but I also have a click listener on each marker and notice that it never fires. I soon realised that leaflet has been updating the marker continuously (the DOM element keeps blinking in the inspector). I attempted to log something to see if the component actually re-renders, but it doesn't. Seems like leaflet is messing with the DOM element under the hood.
const Marker = React.memo(function Marker({ plate, coors, prevCoors }) {
const markerRef = React.useRef();
const [activeVehicle, handleActiveVehicleUpdate] = useActiveVehicle();
const heading = prevCoors != null ? GeoHelpers.computeHeading(prevCoors, coors) : 0;
React.useEffect(() => {
if (prevCoors == null) return;
const [prevLat, prevLong] = prevCoors;
const [lat, long] = coors;
let animationStartTime;
const animateMarker = timestamp => {
if (animationStartTime == null) animationStartTime = timestamp;
const progress = (timestamp - animationStartTime) / 5000;
if (progress > 1) return;
const currLat = prevLat + (lat - prevLat) * progress;
const currLong = prevLong + (long - prevLong) * progress;
const position = new LatLng(currLat, currLong);
markerRef.current.leafletElement.setLatLng(position);
requestAnimationFrame(animateMarker);
};
const animationFrame = requestAnimationFrame(animateMarker);
// eslint-disable-next-line consistent-return
return () => cancelAnimationFrame(animationFrame);
}, [coors, prevCoors]);
React.useEffect(() => {
if (plate === '60C23403') console.log('re-render!');
// eslint-disable-next-line
});
return (
<LeafletMarker
icon={createIcon(plate === activeVehicle, heading)}
position={prevCoors != null ? prevCoors : coors}
onClick={handleActiveVehicleUpdate(plate, coors)}
ref={markerRef}
>
<Tooltip>{plate}</Tooltip>
</LeafletMarker>
);
});
How do I prevent this behaviour from leaflet? Any idea is appreciated. Thanks in advance :)

Resources