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.
Related
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!
I will try to word this in the best way I can...
When I send a function through a prop to a child and then send it again to another child, then use the on click to activate it in the 'grandparent' function. When I console.log in that original function in the grandparent that console.logs a state, it prints undefined, yet when I am within that grandparent and try to activate that function, it will log the state correctly. If anyone can help me a little bit more in depth that would be great, we can call!
import React, { useEffect } from 'react';
import Row from '../row/row';
import './body.css';
import { nanoid } from 'nanoid';
export default function Body () {
const [boxMain, setBoxMain] = React.useState(null)
const [rows, setRows] = React.useState(null)
const ref = React.useRef(null)
function changeBox (event) {
console.log(event);
console.log(boxMain);
}
React.useEffect(() => {
/* Describes array with all information */
const sideBoxes = 40;
const heightContainer = ref.current.offsetHeight
const widthContainer = ref.current.offsetWidth;
const numRows = Math.floor(heightContainer / sideBoxes) - 1
const numBoxes = Math.floor(widthContainer / sideBoxes)
/* Beginning of array birth */
let main = Array(numRows).fill().map(() => new Array(numBoxes).fill({
id: "",
water: false,
land: false,
air: false,
}));
/* End of array birth */
const rows = []
for (let i = 0; i < numRows; i++) {
const id = nanoid();
rows.push(
<Row
key={id}
id={id}
rowNumber={i}
numBoxes={numBoxes}
sideBoxes={sideBoxes}
changeBox={changeBox}
main={main}
/>
)
}
setRows(rows)
setBoxMain(main)
}, [])
return (
<div>
<div onClick={() => changeBox("test")}>
TEST
</div>
<div ref={ref} className='body'>
{rows}
</div>
</div>
)
}
For examples here onClick={() => changeBox("test") the function works and logs "boxMain" correctly. But when I pass changeBox={changeBox} into ...
import React, { useEffect } from "react";
import Box from "../box/box";
import "./row.css";
import { nanoid } from 'nanoid';
export default function Row (props) {
const ref = React.useRef(null)
const [boxes, setBoxes] = React.useState()
useEffect(() => {
const tempBoxes = []
for (let i = 0; i < props.numBoxes; i++) {
const id = nanoid()
tempBoxes.push(
<Box
rowNumber={props.rowNumber}
columnNumber={i}
key={id}
id={id}
side={props.sideBoxes}
changeBox={props.changeBox}
main={props.main}
/>
)
}
setBoxes(tempBoxes)
}, [])
return (
<div ref={ref} className="row-main">
{boxes}
</div>
)
}
Then pass changeBox={props.changeBox} to ...
import React from "react";
import "./box.css";
export default function Box (props) {
React.useEffect(() => {
props.main[props.rowNumber][props.columnNumber] = props.id
}, [])
const [detectChange, setDetectChange] = React.useState(0)
const ref = React.useRef(null)
const styles = {
width: `${props.side}px`,
height: `${props.side}px`,
}
return (
<div
ref={ref}
className="box-main"
key={props.id}
id={props.id}
rownumber={props.rowNumber}
columnnumber={props.columnNumber}
style={styles}
onClick={() => props.changeBox([props.id, props.rowNumber, props.columnNumber])}
>
</div>
)
}
I then have the onClick={() => props.changeBox([props.id, props.rowNumber, props.columnNumber])} and it returns to the original changeBox...
function changeBox (event) {
console.log(event);
console.log(boxMain);
}
but when I click the box it returns the event correctly but returns boxMain as null.
When I click the onClick in the parent function although it console.logs everything correctly.
I know this is a ton of info but I know the fix has to be simple, or at least my method to do this should change.
Thank you for any feedback!! :)
Edit 1:
This is the output normally.
But when I simply add a space to the code and save it in VS Code (I guess some type of rerendering happens?) then it fixes to...
Although the IDs do change so I think everything refreshes in some way.
The useEffect hook of Body component runs only once because it does not have any dependency, thus changeBox callback passed to its children and grand children has the default state of boxMain, and it never updates.
This is why calling changeBox inside Body component logs boxMain array correctly, while calling props.changeBox inside children components logs null.
-------------- Solution ---------------------
This is not the BEST solution, but it will give you an idea why it didn't work before, and how you can fix it.
import React, { useEffect } from 'react';
import Row from '../row/row';
import './body.css';
import { nanoid } from 'nanoid';
export default function Body () {
const [boxMain, setBoxMain] = React.useState(null)
const [rows, setRows] = React.useState(null)
const [rowsData, setRowsData] = React.useState(null)
const ref = React.useRef(null)
function changeBox (event) {
console.log(event);
console.log(boxMain);
}
React.useEffect(() => {
/* Describes array with all information */
const sideBoxes = 40;
const heightContainer = ref.current.offsetHeight
const widthContainer = ref.current.offsetWidth;
const numRows = Math.floor(heightContainer / sideBoxes) - 1
const numBoxes = Math.floor(widthContainer / sideBoxes)
/* Beginning of array birth */
let main = Array(numRows).fill().map(() => new Array(numBoxes).fill({
id: "",
water: false,
land: false,
air: false,
}));
/* End of array birth */
const rowsData = []
for (let i = 0; i < numRows; i++) {
const id = nanoid();
rowsData.push({
key: id,
id,
rowNumber: id,
numBoxes,
sideBoxes,
})
}
setRowsData(rowsData)
setBoxMain(main)
}, [])
React.useEffect(() => {
const rows = []
for (let i = 0; i < rowsData?.length; i++) {
const id = nanoid();
const data = rowsData[i];
rows.push(
<Row
{...data}
changeBox={changeBox}
main={boxMain}
/>
)
}
setRows(rows)
}, [rowsData, boxMain, changeBox])
return (
<div>
<div onClick={() => changeBox("test")}>
TEST
</div>
<div ref={ref} className='body'>
{rows}
</div>
</div>
)
}
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>
I am using usePreviousValue custom hook to get previous props value from my component:
const usePreviousValue = value => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
const MyComponent = ({ count }) => {
const prevCount = usePreviousValue(count)
return (<div> {count} | {prevCount}</div>)
}
But in this case, in prevCount I always have only the first count prop value when a component was rendered, and the next updated prop value is never assigned to it. Are there any ways to properly compare nextProp and prevProp with functional React components?
Your code sample seems to be working just fine. How exactly are you using the component? Try to run the snippet below:
const { useEffect, useRef, useState } = React;
const usePreviousValue = value => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
const MyComponent = ({ count }) => {
const prevCount = usePreviousValue(count);
return (<div> {count} | {prevCount}</div>);
}
function App() {
const [count, setCount] = useState(0);
return (
<div>
<MyComponent count={count} />
<button
onClick={() => setCount((prevCount) => prevCount + 1)}
>
Count++
</button>
</div>
);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
As previously answered, the easiest way to do it is using a custom hook:
import isEqual from "lodash/isEqual";
import { useEffect, useRef } from "react";
const useComponentDidUpdate = (callback, data, checkIfIsEqual) => {
const prevData = useRef(data);
useEffect(() => {
const isTheSame = checkIfIsEqual ? isEqual(data, prevData) : undefined;
callback(prevData.current, isTheSame);
prevData.current = data;
}, [data]);
return null;
};
export default useComponentDidUpdate;
Then in your component:
const Component = ({age})=>{
const [state, setState] = useState({name: 'John', age})
useComponentDidUpdate(prevStateAndProps=>{
if(prevStateAndProps.age !== age || prevStateAndProps.state.name !== state.name){
// do something
}
}, {state, age})
...
}
There are two components, I want to implement an element array using the useContext hook, but when the button is clicked, the element is not removed, but on the contrary, there are more of them. Tell me what is wrong here. I would be very grateful!
First component:
import React from 'react';
import CartItem from './CartItem';
import Context from '../Context';
function Cart() {
let sum = 0;
let arrPrice = [];
let [products, setProducts] = React.useState([]);
let loacalProsucts = JSON.parse(localStorage.getItem('products'));
if(loacalProsucts === null) {
return(
<div className="EmptyCart">
<h1>Cart is empty</h1>
</div>
)
} else {
{loacalProsucts.map(item => products.push(item))}
{loacalProsucts.map(item => arrPrice.push(JSON.parse(item.total)))}
}
for(let i in arrPrice) {
sum += arrPrice[i];
}
function removeItem(id) {
setProducts(
products.filter(item => item.id !== id)
)
}
return(
<Context.Provider value={{removeItem}}>
<div className="Cart">
<h1>Your purchases:</h1>
<CartItem products = {products} />
<h1>Total: {sum}$</h1>
</div>
</Context.Provider>
)
}
Second component:
import React, { useContext } from 'react';
import Context from '../Context';
function CartList({products}) {
const {removeItem} = useContext(Context);
return(
<div className="CartList">
<img src={products.image} />
<h2>{products.name}</h2>
<h3 className="CartInfo">{products.kg}kg.</h3>
<h2 className="CartInfo">{products.total}$</h2>
<button className="CartInfo" onClick={() => removeItem(products.id)}>×</button>
</div>
);
}
export default CartList;
Component with a context:
import React from 'react';
const Context = React.createContext();
export default Context;
Adding to the comment above ^^
It's almost always a mistake to have initialization expressions inside your render loop (ie, outside of hooks). You'll also want to avoid mutating your local state, that's why useState returns a setter.
Totally untested:
function Cart() {
let [sum, setSum] = React.useState();
const loacalProsucts = useMemo(() => JSON.parse(localStorage.getItem('products')));
// Init products with local products if they exist
let [products, setProducts] = React.useState(loacalProsucts || []);
useEffect(() => {
// This is actually derived state so the whole thing
// could be replaced with
// const sum = products.reduce((a, c) => a + c?.total, 0);
setSum(products.reduce((a, c) => a + c?.total, 0));
}, [products]);
function removeItem(id) {
setProducts(
products.filter(item => item.id !== id)
)
}
...