Managing React states correctly giving strange error - reactjs

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
});

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} />
);
};

React screen update from a loop process

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.

React how to wait until state is updated without extra state variables?

I cannot understand. To cut it short, I have these 2 variables which are being used in the state
questionLoaded
gameOver
This component needs to wait until it finishes getting data using the function selectRandomQuestion.
Therefore if i remove the questionLoaded, it will give me exception when it's looping through the map.
Now I had to create another variable to check when the game is over to unrender this component, therefore the terniary condition questionLoaded || gameOver ?
I tried checking the variable currentQuestion to conditionally render, but for some reason it will give exception when looping through the map in the options array of this property. Which means currentQuestion gets data first before it's own child property options has any data when hitting the condition check
The problem is that this looks kind of dirty, I'm still using react just for a few weeks and I'm not sure if this is the appropriate way to deal with state updates or is there a better way to handle it.
To sum it up, my problem is when rendering components, I often need to wait for the state to update when the rendering depends on some conditions, and everytime I need a new condition that means I need another set of logic using another state variable. Which means creating another useEffect with extra logic and to be triggered when that variable is updated.
import React, { useState, useEffect } from 'react';
import './GuessPicture.css';
import questions from './data/questions.js';
function GuessPicture() {
const [currentQuestion, setCurrentQuestion] = useState({});
const [unansweredQuestions, setUnansweredQuestions] = useState([]);
const [answeredQuestions, setAnsweredQuestions] = useState([]);
const [questionLoaded, setQuestionLoaded] = useState(false);
const [gameOver, setGameOver] = useState(false);
useEffect(() => {
setUnansweredQuestions(questions);
selectRandomQuestion();
setQuestionLoaded(true);
}, []);
useEffect(() => {
if (unansweredQuestions.length > 0) nextQuestion();
else alert('game over');
}, [unansweredQuestions]);
function selectRandomQuestion() {
const index = Math.floor(Math.random() * questions.length);
let selectedQuestion = questions[index];
selectedQuestion.options = shuffle(selectedQuestion.options);
setCurrentQuestion(selectedQuestion);
}
function nextQuestion() {
const index = Math.floor(Math.random() * unansweredQuestions.length);
let selectedQuestion = unansweredQuestions[index];
selectedQuestion.options = shuffle(selectedQuestion.options);
setCurrentQuestion(selectedQuestion);
}
// Fisher Yates Shuffle Algorithm
function shuffle(array) {
var currentIndex = array.length,
temporaryValue,
randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
const onClickOption = (event) => {
if (currentQuestion.correctValue == event.target.dataset.value) {
setAnsweredQuestions((answeredQuestions) => [
...answeredQuestions,
currentQuestion,
]);
const newUnansweredQuestions = unansweredQuestions.filter(
(item) => item.id != currentQuestion.id
);
setUnansweredQuestions(newUnansweredQuestions);
} else alert('Wrong');
};
return (
<>
{questionLoaded || gameOver ? (
<div className="guess-picture-container">
<div className="guess-picture">
<img src={currentQuestion.image} alt="English 4 Fun" />
</div>
<div className="guess-picture-answers-grid">
{currentQuestion.options.map((key) => (
<button
key={key.value}
onClick={onClickOption}
data-value={key.value}
>
{key.display}
</button>
))}
</div>
</div>
) : null}
</>
);
}
export default GuessPicture;
Your code looks fine to me, however I would avoid the ternary operator in this situation and use the short-circuit operator instead. This makes your code a little cleaner.
instead of:
{questionLoaded || gameOver ? <SomeComponentHere /> : null}
try using:
{(questionLoaded || gameOver) && <SomeComponentHere />}
Why avoid the ternary operator? Because it's not a ternary operation, it is a boolean check. This makes your code more semantically appropriate.

Which SectionHeader is Sticky in react-native?

I'm using React-Native's SectionList component to implement a list with sticky section headers. I'd like to apply special styling to whichever section header is currently sticky.
I've tried 2 methods to determine which sticky header is currently "sticky," and neither have worked:
I tried to use each section header's onLayout prop to determine each of their y offsets, and use that in combination with the SectionList onScroll event to calculate which section header is currently "sticky".
I tried to use SectionList's onViewableItemsChanged prop to figure out which header is currently sticky.
Approach #1 doesn't work because the event object passed to the onLayout callback only contains the nativeEvent height and width properties.
Approach #2 doesn't work because the onViewableItemsChanged callback appears to be invoked with inconsistent data (initially, the first section header is marked as viewable (which it is), then, once it becomes "sticky," it is marked as not viewable, and then with further scrolling it is inexplicable marked as viewable again while it is still "sticky" [this final update seems completely arbitrary]).
Anyone know a solution that works?
While you creating stickyHeaderIndices array in render() method you should create an object first. Where key is index and value is offset of that particular row, eg. { 0: 0, 5: 100, 10: 200 }
constructor(){
super(props);
this.headersRef = {};
this.stickyHeadersObject = {};
this.stickyHeaderVisible = {};
}
createHeadersObject = (data) => {
const obj = {};
for (let i = 0, l = data.length; i < l; i++) {
const row = data[i];
if (row.isHeaderRow) {
obj[i] = row.offset;
}
}
return obj;
};
createHeadersArray = data => Object.keys(data).map(str => parseInt(str, 10))
render() {
const { data } = this.props;
// Expected that data array already has offset:number and isHeaderRow:boolean values
this.stickyHeadersObject = this.createHeadersObject(data);
const stickyIndicesArray = this.createHeadersArray(this.stickyIndicesObject);
const stickyHeaderIndices = { stickyHeaderIndices: stickyIndicesArray };
return (<FlatList
...
onScroll={event => this.onScroll(event.nativeEvent.contentOffset.y)}
{...stickyHeaderIndices}
...
renderItem={({ item, index }) => {
return (
{ // render header row
item.isHeaderRow &&
<HeaderRow
ref={ref => this.headersRef[index] = ref}
/>
}
{ // render regular row
item.isHeaderRow &&
<RegularRow />
}
);
}}
/>)
Then you have to monitor is current offset bigger than your "titleRow"
onScroll = (offset) => {
Object.keys(this.stickyHeadersObject).map((key) => {
this.stickyHeaderVisible[key] = this.stickyHeadersObject[key] <= offset;
return this.headersRef[key] && this.headersRef[key].methodToUpdateStateInParticularHeaderRow(this.stickyHeaderVisible[key]);
});
};

Resources