Array disappears after a few clicks with an undefined error - React - javascript

I'm trying to make a simulation of a blackjack hand - first user get two random cards, and with each 'hit' they get another one, however after a few 'hit' the app crashes and the 'undefined' error comes up in (array is undefined therefore can't get length). I've tried to save the deck again in the original shuffle, tried putting it all in one, however I can never get it to fully work.
I suspect it's something to do with useState being used incorrectly, however I'm not sure how to fix it.
Here's my code:
import {useState, useEffect} from 'react'
import Card from '../components/Card';
import Total from '../components/Total';
import {deckArray} from '../utils/data'
export default function Home(){
const initialHand = 2
const [dealCards, setDealCards] = useState(false)
const [isStarted, setIsStarted] = useState(false)
const [isReset, setIsReset] = useState(false)
const [hand, setHand] = useState(initialHand)
const [deck, setDeck] = useState(deckArray)
const [total, setTotal] = useState(0)
const [usersCards, setUsersCards] = useState([])
function shuffle(deck){
console.log("shuffle called")
setIsStarted(true)
let i = deck.length;
while (--i > 0) {
let randIndex = Math.floor(Math.random() * (i + 1));
[deck[randIndex], deck[i]] = [deck[i], deck[randIndex]];
}
setUsersCards(deck.slice(-initialHand))
console.log(usersCards)
console.log(deck)
}
useEffect(() => {
if(dealCards===true){
const randomCard = deck[Math.floor(Math.random()*deck.length)];
const newCardsArray = deck.filter(el => el.index !== randomCard.index)
const chosenCardArray = deck.filter(el => el.index === randomCard.index)
const chosenCard = chosenCardArray[0]
setDeck(newCardsArray)
setUsersCards(prevCards => [...prevCards, chosenCard])
console.log(newCardsArray.length)
setDealCards(false)
}
}, [usersCards, dealCards, deck])
useEffect(() => {
if(isReset){
setUsersCards([])
setDeck(shuffle(deckArray))
setDealCards(false)
setTotal(0)
setIsStarted(true)
}
setIsReset(false)
},[isReset, setIsReset])
useEffect(() => {
if(total>=22){
setIsStarted(true)
setIsReset(true)
setDeck(shuffle(deckArray))
}
}, [total, setTotal])
return (
<>
{isStarted ? <>
<Total usersCards={usersCards} total={total} setTotal={setTotal}/>
<Card usersCards={usersCards} />
<button onClick={() => setDealCards(true)}>HIT</button>
<button>STAND</button>
<button onClick={() => setIsReset(true)}>START OVER</button>
</> :
<>
<p>Game over!</p>
<button onClick={() => shuffle(deck)}>PLAY AGAIN</button></>}
</>
)
}
any help much appreciated!

Related

Pagination with React doesn't work, all items are still displayed on screen

I have a pagination made with a react-paginate package called react-paginate here is the link to the doc. https://www.npmjs.com/package/react-paginate
I have implemented it in my App which is a notes diary, the user creates notes and these are dynamically saved in the localStorage and displayed on screen, well, I have established that there are 6 notes per page, that is, when there is a seventh note, it should not be displayed unless the user goes to page 2, when there are 13 notes page 3 and so ...
The functionality of my component that I have called Pagination works correctly, it is dynamic, I have right now testing 13 notes, so it shows me 3 pages, if I had 12, it would show me 2.
The problem is that although my pagination is correct, the 13 notes are being shown on the screen, when it should be 6 - 6 - 1.
I leave you the code to see if we can find the error, greetings and thanks in advance.
The prop that Pagination receives called data, are basically the notes that are created dynamically in App.js. const [notes, setNotes] = useState([]);
Component Pagination
import React, { useEffect, useState } from 'react'
import ReactPaginate from 'react-paginate';
import '../styles/Pagination.css';
const Pagination = (props) => {
const { data } = props;
// We start with an empty list of items.
const [currentItems, setCurrentItems] = useState([]);
const [pageCount, setPageCount] = useState(0);
// Here we use item offsets; we could also use page offsets
// following the API or data you're working with.
const [itemOffset, setItemOffset] = useState(0);
const itemsPerPage = 6;
useEffect(() => {
// Fetch items from another resources.
const endOffset = itemOffset + itemsPerPage;
console.log(`Loading items from ${itemOffset} to ${endOffset}`);
setCurrentItems(data.slice(itemOffset, endOffset));
setPageCount(Math.ceil(data.length / itemsPerPage));
}, [itemOffset, itemsPerPage, data]);
// Invoke when user click to request another page.
const handlePageClick = (event) => {
const newOffset = (event.selected * itemsPerPage) % data.length;
console.log(
`User requested page number ${event.selected}, which is offset ${newOffset}`
);
setItemOffset(newOffset);
};
return (
<>
<ReactPaginate
breakLabel="..."
nextLabel="next >"
onPageChange={handlePageClick}
pageRangeDisplayed={3}
pageCount={pageCount}
previousLabel="< previous"
renderOnZeroPageCount={null}
containerClassName="pagination"
pageLinkClassName="page-num"
previousLinkClassName="page-num"
nextLinkClassName="page-num"
activeLinkClassName="activee boxx"
/>
</>
);
}
export default Pagination;
Component App
import { useState, useEffect } from "react";
import { nanoid } from 'nanoid';
import NoteList from "./components/NoteList";
import './App.css';
import Search from "./components/Search";
import Header from "./components/Header";
import Pagination from "./components/Pagination";
const App = () => {
const [notes, setNotes] = useState([]);
const [searchText, setSearchText] = useState('');
const [darkMode, setDarkMode] = useState(false);
//Se encarga de mostrar la nota para escribir
const [showNote, setShowNote] = useState(true); //eslint-disable-line
useEffect(() => {
const saveNotes = JSON.parse(localStorage.getItem('notes-data'));
if (saveNotes){
setNotes(saveNotes);
}
}, []);
useEffect(() => {
localStorage.setItem('notes-data', JSON.stringify(notes))
},[notes])
const addNote = (inputText, text) => {
const date = new Date();
const newNote = {
id: nanoid(),
title: inputText,
text: text,
date: date.toLocaleString()
}
const newNotes = [newNote, ...notes];
setNotes(newNotes)
}
const deleteNote = (id) => {
var response = window.confirm("Are you sure that you want to remove the note?");
if (response){
const notesUpdated = notes.filter((note) => note.id !== id)
setNotes(notesUpdated);
}
}
return (
<div className={darkMode ? 'dark-mode' : ''}>
<div className="container">
<Header
handleToggleTheme={setDarkMode}
/>
<Search
handleSearchNote={setSearchText}
setShowNote={setShowNote}
/>
<NoteList
notes={notes.filter((noteText) =>
noteText.title.toLowerCase().includes(searchText)
)}
handleAddNote={addNote}
handleDeleteNote={deleteNote}
/>
<Pagination data={notes}/>
</div>
</div>
)
}
export default App;
The problem is you are not using currentItems and the paginated data is stored in that state.
Codesandbox: https://codesandbox.io/s/sweet-keldysh-2u72vd
Pagination.js
import React, { useEffect, useState } from 'react'
import ReactPaginate from 'react-paginate';
import NoteList from "./components/NoteList";
import '../styles/Pagination.css';
const Pagination = (props) => {
const { data, searchText, handleAddNote, handleDeleteNote } = props;
// We start with an empty list of items.
const [currentItems, setCurrentItems] = useState([]);
const [pageCount, setPageCount] = useState(0);
// Here we use item offsets; we could also use page offsets
// following the API or data you're working with.
const [itemOffset, setItemOffset] = useState(0);
const itemsPerPage = 6;
useEffect(() => {
// Fetch items from another resources.
const endOffset = itemOffset + itemsPerPage;
console.log(`Loading items from ${itemOffset} to ${endOffset}`);
setCurrentItems(data.slice(itemOffset, endOffset));
setPageCount(Math.ceil(data.length / itemsPerPage));
}, [itemOffset, itemsPerPage, data]);
// Invoke when user click to request another page.
const handlePageClick = (event) => {
const newOffset = (event.selected * itemsPerPage) % data.length;
console.log(
`User requested page number ${event.selected}, which is offset ${newOffset}`
);
setItemOffset(newOffset);
};
return (
<>
<NoteList
notes={currentItems.filter((noteText) =>
noteText.title.toLowerCase().includes(searchText)
)}
handleAddNote={handleAddNote}
handleDeleteNote={handleDeleteNote}
/>
<ReactPaginate
breakLabel="..."
nextLabel="next >"
onPageChange={handlePageClick}
pageRangeDisplayed={3}
pageCount={pageCount}
previousLabel="< previous"
renderOnZeroPageCount={null}
containerClassName="pagination"
pageLinkClassName="page-num"
previousLinkClassName="page-num"
nextLinkClassName="page-num"
activeLinkClassName="activee boxx"
/>
</>
);
}
export default Pagination;
App.js
import { useState, useEffect } from "react";
import { nanoid } from 'nanoid';
import './App.css';
import Search from "./components/Search";
import Header from "./components/Header";
import Pagination from "./components/Pagination";
const App = () => {
const [notes, setNotes] = useState([]);
const [searchText, setSearchText] = useState('');
const [darkMode, setDarkMode] = useState(false);
//Se encarga de mostrar la nota para escribir
const [showNote, setShowNote] = useState(true); //eslint-disable-line
useEffect(() => {
const saveNotes = JSON.parse(localStorage.getItem('notes-data'));
if (saveNotes){
setNotes(saveNotes);
}
}, []);
useEffect(() => {
localStorage.setItem('notes-data', JSON.stringify(notes))
},[notes])
const addNote = (inputText, text) => {
const date = new Date();
const newNote = {
id: nanoid(),
title: inputText,
text: text,
date: date.toLocaleString()
}
const newNotes = [newNote, ...notes];
setNotes(newNotes)
}
const deleteNote = (id) => {
var response = window.confirm("Are you sure that you want to remove the note?");
if (response){
const notesUpdated = notes.filter((note) => note.id !== id)
setNotes(notesUpdated);
}
}
return (
<div className={darkMode ? 'dark-mode' : ''}>
<div className="container">
<Header
handleToggleTheme={setDarkMode}
/>
<Search
handleSearchNote={setSearchText}
setShowNote={setShowNote}
/>
<Pagination data={notes} handleAddNote={addNote}
handleDeleteNote={deleteNote} searchText={searchText} />
</div>
</div>
)
}
export default App;

How callIndex works in react's useState under the hood?

I'm currently learning how React works under the hood and built a custom useState from scratch following the tutorials on Youtube.
But I'm having trouble understanding how callIndex is incremented and why do I need to set currentIndex variables which basically take the callIndex value.
The code works like this -
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
let callIndex = -1;
let stateValues = [];
const useState = (initValue) => {
callIndex++;
console.log("callIndex", callIndex);
const currentIdx = Number(callIndex);
if (stateValues[currentIdx] === undefined) {
stateValues[currentIdx] = initValue;
}
const setState = (newValue) => {
console.log("currentIdx in setState", currentIdx);
stateValues[callIndex] = newValue;
render();
};
return [stateValues[currentIdx], setState];
};
const App = () => {
const [countA, setCountA] = useState(1);
const [countB, setCountB] = useState(-1);
const [countC, setCountC] = useState(0);
return (
<div>
<div>
<h1>Count A: {countA}</h1>
<button onClick={() => setCountA(countA - 1)}>Subtract</button>
<button onClick={() => setCountA(countA + 1)}>Add</button>
</div>
<div>
<h1>Count B: {countB}</h1>
<button onClick={() => setCountB(countB - 1)}>Subtract</button>
<button onClick={() => setCountB(countB + 1)}>Add</button>
</div>
<div>
<h1>Count C: {countC}</h1>
<button onClick={() => setCountC(countC - 1)}>Subtract</button>
<button onClick={() => setCountC(countC + 1)}>Add</button>
</div>
</div>
);
};
function render() {
// callIndex = -1;
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
}
render();
Every time the useState function is called, the callIndex variables would be incremented by 1.
In this code, I called useState 3 times. So, the callIndex value would be 3.
The problem starts here - when I called the setState function, it'll first assign the newValue to the stateValues but in which index will it be changed.
Are there any articles or tutorials explaining how this happening?
The biggest issue is, you need to be able to distinguish between useState calls for different components, and different renders of the same component. You need this
<Comp1 />
const Comp1 = () => {
const [countA, setCountA] = useState(1);
const [countB, setCountB] = useState(-1);
to perform different logic than
<Comp1 />
<Comp2 />
const Comp1 = () => {
const [countA, setCountA] = useState(1);
const Comp2 = () => {
const [countB, setCountB] = useState(-1);
But the information available to you exposed by React with functional components is very limited, and won't allow you to do this easily. There is much more information used by (and available to) React's internals, which can distinguish things like the start of a component call.
If you want to "replicate" useState, I think the easiest way to do so would be to go outside of React completely, so that when "updating" the state, you can just call a function that re-populates a section of the DOM. (In React, you don't want to call ReactDOM.render on an app many times, and especially not on every render - that results in it being rebuilt from the ground up every time.)
Also, a small issue - when mounting, you should check if the state property exists on the array before assigning, not just if the value on that property is undefined, otherwise you'll lose the desired logic when nothing is passed as an initial value. This
if (stateValues[currentIdx] === undefined) {
stateValues[currentIdx] = initValue;
}
should be
if (!([currentIdx in stateValues)) {
stateValues[currentIdx] = initValue;
}
In your state setter, you also need to change
stateValues[callIndex] = newValue;
to
stateValues[currentIdx] = newValue;
For a vanilla JS demo of your tweaked code:
let callIndex = -1;
let stateValues = [];
const useState = (initValue) => {
callIndex++;
const currentIdx = Number(callIndex);
if (!(currentIdx in stateValues)) {
stateValues[currentIdx] = initValue;
}
const setState = (newValue) => {
stateValues[currentIdx] = newValue;
renderApp();
};
return [stateValues[currentIdx], setState];
};
const root = document.querySelector('.root');
const renderApp = () => {
callIndex = -1;
root.textContent = '';
root.appendChild(App());
};
const App = () => {
const [countA, setCountA] = useState(1);
const [countB, setCountB] = useState(-1);
const [countC, setCountC] = useState(0);
const div = document.createElement('div');
div.innerHTML = `
<div>
<h1>Count A: ${countA}</h1>
<button>Subtract</button>
<button>Add</button>
</div>
<div>
<h1>Count B: ${countB}</h1>
<button>Subtract</button>
<button>Add</button>
</div>
<div>
<h1>Count C: ${countC}</h1>
<button>Subtract</button>
<button>Add</button>
</div>`;
// this could be made less repetitive, but this is just for demo purposes
const buttons = div.querySelectorAll('button');
buttons[0].onclick = () => setCountA(countA - 1);
buttons[1].onclick = () => setCountA(countA + 1);
buttons[2].onclick = () => setCountB(countB - 1);
buttons[3].onclick = () => setCountB(countB + 1);
buttons[4].onclick = () => setCountC(countC - 1);
buttons[5].onclick = () => setCountC(countC + 1);
return div;
};
renderApp();
<div class="root"></div>

LocalStorage includes an empty array on every render

I'm trying to update my localStorage with new items that are added to my shopping cart. However, every time an item is added, an empty array is added before it (see screenshot). Why is this?
I'm thinking I need to add a ternary operator to return an empty array if there are no existing items in the cart and to return the current items in the cart if there are items currently in localStorage. Is this correct, or do I have a syntax error?
Screenshot:
Code in question:
useEffect(() => {
const newData = JSON.parse(localStorage.getItem('product')) || [];
newData.push(cart);
localStorage.setItem('product', JSON.stringify(newData));
}, [cart])
Full code:
import React, { useState, useEffect } from 'react';
import './../App.css';
import * as ReactBootStrap from 'react-bootstrap';
function Cart(props) {
const [cart, setCart] = useState([]);
const [quantity, setQuantity] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(async () => {
fetchItems();
}, [])
const itemId = props.match.params.id;
const itemQuantity = parseInt(props.match.params.qty, 10);
const fetchItems = async () => {
const data = await fetch('https://fakestoreapi.com/products/' + itemId);
const items = await data.json();
setCart(items)
setQuantity(itemQuantity)
setLoading(true)
}
function price(qty){
const newPrice = qty * cart.price;
return newPrice.toFixed(2)
}
useEffect(() => {
const newData = JSON.parse(localStorage.getItem('product')) || [];
newData.push(cart);
localStorage.setItem('product', JSON.stringify(newData));
}, [cart])
return (
<div>
{loading ? (
<div className="productStyle">
<img src={cart.image} className="productImage"></img>
<p>{cart.title}</p>
<div className="quantity">
<button className="btn minus-btn" type="button"
onClick={quantity > 1 ? () => setQuantity(quantity - 1) : null}>-</button>
<input type="text" id="quantity" placeholder={quantity}/>
<button className="btn plus-btn" type="button"
onClick={() => setQuantity(quantity + 1)}>+</button>
</div>
<p>${price(quantity)}</p>
</div>
) : (<ReactBootStrap.Spinner className="spinner" animation="border" />)}
</div>
);
}
export default Cart;
You are fetching the initial cart content asynchronously, but are setting product to newData right away, i.e., before cart had any content, which means that newData only contains [].
Perhaps just replace newData.push(cart); with cart.length > 0 && newData.push(cart);? Or rethink your logic. Not sure what you want product to contain.

React Component is not re-rendering on the first state update

I'm currently working on a very simple survey. The kind that you reply by Yes or No. I have made a list of questions that I have put and extracted in its own file (QuestionsList.js).
Here is my question list:
const QuestionsList = [
"Do you believe in ghosts? ",
"Have you ever seen a UFO? ",
"Can cats jump six times their length? "
]
export default QuestionsList
I have my App:
import './App.css';
import Question from './components/Question';
import QuestionsList from './QuestionsList'
import { useState } from 'react';
function App() {
let questionsList = QuestionsList
const [current, setCurrent] = useState(0)
const [currentQuestion, setCurrentQuestion] = useState(null)
const [answers, setAnswers] = useState([])
const [isStarted, setIsStarted] = useState(false)
const onStartHandler = () => {
setIsStarted(true)
updateCurrentQuestion()
}
const updateCurrentQuestion = () => {
setCurrentQuestion(questionsList[current])
}
const onYesHandler = () => {
setCurrent(current => current += 1)
setAnswers([...answers, 1])
updateCurrentQuestion()
}
const onNoHandler = () => {
setCurrent(current => current += 1)
setAnswers([...answers, 0])
updateCurrentQuestion()
}
return (
<div className="App">
{isStarted ? <Question question={currentQuestion} onYes={onYesHandler} onNo={onNoHandler} /> : null}
<button onClick={onStartHandler}>START!</button>
<button onClick={() => console.log(`Current: ${current}\nCurrent Question: ${currentQuestion}\nAnswers: ${answers}`)}>STATE LOG</button>
</div>
);
}
export default App;
And my Question component:
import React from 'react'
const Question = (props) => {
return (
<div>
<h2>{props.question}</h2>
<div>
<button onClick={props.onYes}>YES</button>
<button onClick={props.onNo}>NO</button>
</div>
</div>
)
}
export default Question
The problem is that whenever I launch the app. The first question shows up, but on the very FIRST click on YES or NO, the state changes and so does the question, but the FIRST click, does not rerender the question. However, every subsequent click does re-render the component. What am I missing?
When you call setCurrentQuestion() it uses the previous value of current because setting the state (which setCurrent does) is async.
You don't need the currentQuestion state, because it's derived from current. Use the current value to get the question from the questionsList.
const { useState } = React;
const Question = (props) => {
return (
<div>
<h2>{props.question}</h2>
<div>
<button onClick={props.onYes}>YES</button>
<button onClick={props.onNo}>NO</button>
</div>
</div>
)
}
function App({ questionsList }) {
const [current, setCurrent] = useState(0)
const [answers, setAnswers] = useState([])
const [isStarted, setIsStarted] = useState(false)
const onStartHandler = () => {
setIsStarted(true)
}
const onYesHandler = () => {
setCurrent(current => current += 1)
setAnswers([...answers, 1])
}
const onNoHandler = () => {
setCurrent(current => current += 1)
setAnswers([...answers, 0])
}
return (
<div className="App">
{isStarted ? (
<Question
question={questionsList[current]}
onYes={onYesHandler}
onNo={onNoHandler} />
)
:
(
<button onClick={onStartHandler}>START!</button>
)
}
</div>
);
}
const QuestionsList = [
"Do you believe in ghosts? ",
"Have you ever seen a UFO? ",
"Can cats jump six times their length? "
];
ReactDOM.render(
<App questionsList={QuestionsList} />,
root
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
Set initial state
the simple solution:
replace this
const [currentQuestion, setCurrentQuestion] = useState(null)
in this const [currentQuestion, setCurrentQuestion] = useState(questionsList[current])
BTW
I see some things to improve.
the onYesHandler onNoHandler can be one function that get boolean (true or false for yes or no)
You don't need to save the data you import let questionsList = QuestionsList. you can use it from the import as QuestionsList
but it's looks ok for a beginner :)
const updateCurrentQuestion = (num) => {
setCurrentQuestion(questionsList[num])
}
// within no/yes functions
let num = current;
setCurrent(old => old + 1);
updateCurrentQuestion(num);
This should fix it. UpdateCurrentQuestion is getting past state.

Display a random string from an array on the click of a button [React]

I'm trying to build a simple app that displays a random question. I can successfully display a random string from an array on mount and page refresh.
I would like to display a different random question 'onClick' of a button rather than refresh the page?
Heres the code so far:
export const QuestionContainer = () => {
const [response, setResponse] = useState({});
useEffect(() => {
fetchData().then(res => setResponse(res));
}, []);
const { records = [] } = response;
const questions = records.map(record => record.fields.question);
console.table(questions);
// const randomNum = arr => {
// return Math.floor(Math.random() * arr.length);
// };
return (
<div className='questions-container'>
<h1>{questions[0]}?</h1>
<button onClick={() => console.log('more')}>More</button>
</div>
);
};
The const questions is an array of strings example - ['hello', 'world', 'noob question']
You're very close! Just use the onClick to set the index that's being displayed, and you're golden! This should work:
const randomIndex = (arr) => { // returns a random int value to use as an index
return Math.floor(Math.random() * arr.length)
}
export const QuestionContainer = () => {
const [response, setResponse] = useState({});
const [index, setIndex] = useState(0) // 0 initially, as you had in your example
useEffect(() => {
fetchData().then(res => setResponse(res));
}, []);
const { records = [] } = response;
const questions = records.map(record => record.fields.question);
console.table(questions);
return (
<div className='questions-container'>
<h1>{questions[index]}?</h1>
<button onClick={_ => setIndex(randomIndex(questions)}>More</button>
</div>
);
};
If you use another API Random Quote with a button:
import './App.css';
import axios from 'axios';
import { useEffect, useState } from 'react';
const App = () => {
const randomIndex = (arr) => { // returns a random int value to use as an index
return Math.floor(Math.random() * arr.length)
}
const [quote, setQuote] = useState("");
const [index, setIndex] = useState(0);
const quoteAPI = async () => {
let arrayOfQuotes = [];
try {
const data = await axios.get("https://raw.githubusercontent.com/skolakoda/programming-quotes-api/master/backup/quotes.json");
arrayOfQuotes = data.data;
console.log(arrayOfQuotes);
const quote = arrayOfQuotes.map((arrayOfQuote) =>
<div key={arrayOfQuote.id}>
<h3>{arrayOfQuote.en}</h3>
<p>{arrayOfQuote.author}</p>
</div>);
console.log(quote);
setQuote(quote);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
quoteAPI();
}, []);
return (<div className="App">
<h4>{quote[index]}</h4>
<button onClick={_ => setIndex(randomIndex(quote))}>Get Random Quote</button>
</div>
);
};
export default App;

Categories

Resources