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

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>

Related

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

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!

Get previous props value with React Hooks

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

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.

Clear button in React clears input but doesn't reset the array element

I try to make a simple meme generator where a user can add a text and change the image on click. Both is working but my clear-button only clears the input field and don't get back to the first image (array[o]).
I mean if I conole.log the "element" it says "0" but it don't change the image to the first one.
My code of App.js so far:
import React, { useEffect, useState } from "react";
import "./styles.css";
function useCounter(initialCount = 0) {
const [count, setCount] = React.useState(initialCount);
const increment = React.useCallback(() => setCount((c) => c + 1), []);
return { count, increment };
}
export default function App() {
let { count: element, increment } = useCounter(0);
const [memes, setMemes] = useState([]);
const [topText, setTopText] = useState("");
useEffect(() => {
async function asyncFunction() {
const initialResponse = await fetch("https://api.imgflip.com/get_memes");
const responseToJSON = await initialResponse.json();
setMemes(responseToJSON.data.memes);
}
asyncFunction();
}, []);
const clear = (e) => {
setTopText("");
element = 0;
console.log(element);
};
return (
<div className="App">
{memes[0] ? (
<div
style={{
height: "300px",
backgroundImage: `url(${memes[element].url})`
}}
>
<p>{topText}</p>
<input
value={topText}
onChange={(e) => setTopText(e.target.value)}
type="text"
/>
<button onClick={clear} type="reset">Clear</button>
<button onClick={increment}>Change Image</button>
</div>
) : (
"loading"
)}
</div>
);
}
What is wrong?
You are attempting to mutate state. You should never directly assign a new value to a stateful variable element = 0. You should use the provided updater function from useState (setCount).
One solution would be to add a reset function to your custom hook and use it:
function useCounter(initialCount = 0) {
const [count, setCount] = React.useState(initialCount);
const increment = React.useCallback(() => setCount((c) => c + 1), []);
const reset = () => setCount(initialCount);
return { count, increment, reset };
}
In your component:
const { count: element, increment, reset: resetCount } = useCounter(0);
const clear = (e) => {
setTopText("");
resetCount();
};
Notice I've also changed the custom hook to use a const instead of let. This is recommended to encourage immutable usage of state, and give helpful errors when breaking that rule.

React Hooks setState of an element in an array

How can I update a single element in a state array? Here is the code that I am currently using:
const Cars = props => {
const [cars, setCars] = React.useState(["Honda","Toyota","Dodge"])
const handleClick1 = () => { setCars[0]("Jeep") }
const handleClick2 = () => { setCars[1]("Jeep") }
const handleClick3 = () => { setCars[2]("Jeep") }
return (
<div>
<button onClick={handleClick1}>{cars[0]}</button>
<button onClick={handleClick2}>{cars[1]}</button>
<button onClick={handleClick3}>{cars[2]}</button>
</div>
)
};
When I click one of the rendered buttons, I get Uncaught TypeError: setCars[0] is not a function at handleClick1.
I know how to do this in a React Class, but how can I do this with React Hooks?
I suggest you map through your cars in order to render them - this is just overall a million times easier. From there you can apply an onClick handler to each button..
Furthermore, you should not mutate state like you are - always make a copy of state first, update the copy, then set your new state with the updated copy.
Edit: one thing that slipped my mind before was adding a key to each item when you are mapping over an array. This should be standard practice.
const { useState } = React;
const { render } = ReactDOM;
const Cars = props => {
const [cars, setCars] = useState(["Honda", "Toyota", "Dodge"]);
const updateCars = (value, index) => () => {
let carsCopy = [...cars];
carsCopy[index] = value;
setCars(carsCopy);
};
return (
<div>
{cars && cars.map((c, i) =>
<button key={`${i}_${c}`} onClick={updateCars("Jeep", i)}>{c}</button>
)}
<pre>{cars && JSON.stringify(cars, null, 2)}</pre>
</div>
);
};
render(<Cars />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>
I think you should correct these lines to spot the source of error
const handleClick1 = () => { setCars[0]("Jeep") }
into
const handleClick1 = () => { cars[0]="Jeep"; setCars(cars); }

Categories

Resources