Replicating React useState on Vanilla Javascript/Node - javascript

I'm trying to get back the state value from this useState Vanilla JS replica:
let callIndex = -1
const stateValues = []
const useState = (initialValue) => {
callIndex++ // 0
const currentCallIndex = Number(callIndex) // 0
if (stateValues[currentCallIndex] === undefined){
stateValues[currentCallIndex] = initialValue
}
const setValue = (newValue) => {
stateValues[currentCallIndex] = newValue
}
return [stateValues[currentCallIndex], setValue]
}
const [countA, setCountA] = useState(1)
const [countB, setCountB] = useState(-1)
This useState() function returns an initializing value and storing it in the stateValues array. But it doesn't actually refer to the array position.
setCountA(3)
// LOGs-> stateValues[0]: 3, stateValues[1]: -1, countA: 1, countB: -1
setCountA(5)
setCountB(9)
// LOGs-> stateValues[0]: 5, stateValues[1]: 9, countA: 1, countB: -1
setCountA(2)
setCountB(5)
// LOGs-> stateValues[0]: 2, stateValues[1]: 5, countA: 1, countB: -1
Now, I can make useState return an arrow function that returns stateValues[currentCallIndex] but then I have to call a function every time I need a state value.
Is there a more Reactsy way of returning the updated value by reference (countA rather than countA() )?

If you insist on doing it the React way without implementing an actual component lifecycle logic, you can do something like this:
let callIndex = -1
const states = []
const useState = (initialValue) => {
const currentCallIndex = callIndex++
states[currentCallIndex] = states[currentCallIndex] ?? {
value: initialValue,
setValue: (newValue) => {
states[currentCallIndex].value = newValue;
},
}
return states[currentCallIndex]
}
let stateA = useState(1)
let stateB = useState(-1)
console.log(stateA.value, stateB.value) // 1, -1
stateA.setValue(2)
stateB.setValue(3)
console.log(stateA.value, stateB.value) // 2, 3
// ======= now let's reset the index =======
callIndex = -1;
// ======= and check that the state is persistent =======
stateA = useState(1)
stateB = useState(-1)
console.log(stateA.value, stateB.value) // 2, 3

Related

How to fix Type 'null' cannot be used as an index type

So I'm building a quiz app and that quiz is divided into difficulty levels. I change that level on a button click by setting params into my path. Then I try to insert a difficulty index because the structure of my obj is:
const questions = {
easy: [...],
medium: [...],
hard: [...]
}
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(0);
const difficulty = searchParams.get("difficulty");
const handlePageChange = () => {
if (currentPage + 1 < questions[difficulty].length) {
setCurrentPage((prevState) => prevState + 1);
}
};
const handleDifficultyChoice = (difficulty: DifficultyType) => {
setSearchParams({ difficulty });
};
Unfortunately I cannot insert that index because index cannot be null. How to solve that?
You can replace the contents of the handlePageChange function with the following code, which will guarantee that difficulty is a valid property key in questions before executing the code after the conditional expression:
if (
// This expresion evaluates to true or false,
// depending on whether difficulty is a valid property key in questions:
(difficulty as DifficultyType) in questions
// If it evaluates to false, this one won't execute:
&& currentPage + 1 < questions[difficulty as DifficultyType].length
// and neither will the setCurrentPage invocation:
) setCurrentPage((prevState) => prevState + 1);
Reference: The difficulty as DifficultyType syntax is what's called a type assertion.
Here's a full, reproducible example in the TypeScript Playground:
import {useState} from 'react';
import {useSearchParams} from 'react-router-dom';
const questions = {
easy: [],
medium: [],
hard: []
};
type DifficultyType = keyof typeof questions;
function Component () {
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(0);
const difficulty = searchParams.get("difficulty");
const handlePageChange = () => {
if (
(difficulty as DifficultyType) in questions
&& currentPage + 1 < questions[difficulty as DifficultyType].length
) setCurrentPage((prevState) => prevState + 1);
};
const handleDifficultyChoice = (difficulty: DifficultyType) => {
setSearchParams({ difficulty });
};
}
See also: type guard functions in the TS handbook
URLSearchParams.get returns a string value or null.
Check if difficulty is truthy prior to using it as a dynamic key.
Example:
const handlePageChange = () => {
if (difficulty && currentPage + 1 < questions[difficulty]?.length) {
setCurrentPage((prevState) => prevState + 1);
}
};

Not sure how to write this function in hooks form

I am converting a code from class to functional component. I am unsure as to how I would convert the code below into a hooks function.
applyFilter = () => {
const { filterLimit: value, lessThanOrGreaterThan } = this.state;
const isLessThan = lessThanOrGreaterThan === "lessThan";
// update instance variable
this.state.datas = this.state.datas.map(v => {
if (isLessThan ? v <= value : v >= value) return v;
return 0;
});
this.setState(prevState => ({
datas: this.state.datas,
}));
}
I have declared the used variables as hooks as follows-
const [filterLimit, setfilterLimit] = useState(100);
const [lessThanOrGreaterThan, setlessThanOrGreaterThan] = useState('lessThan');
const [datas, setDatas] = useState([12, 19, 3, 5, 2, 10])
Looks like you've got most of is. Looks like you just need to swap out the references to state with the props you've already defined.
const [filterLimit, setfilterLimit] = useState(100);
const [lessThanOrGreaterThan, setlessThanOrGreaterThan] = useState('lessThan');
const [datas, setDatas] = useState([12, 19, 3, 5, 2, 10])
const applyFilter = () => {
const isLessThan = lessThanOrGreaterThan === "lessThan";
// update instance variable
const newDatas = datas.map(v => {
if (isLessThan ? v <= value : v >= value) return v;
return 0;
});
setDatas(newDatas);
}

How to correctly set state in a loop in react functional component using useState

Consider I'm uploading a bunch of files and I'm trying to keep track of the ones in progress so that whenever an upload is finished, I decrement my state until all are finished and finally I change my uploading state to false:
(Here I made a sleep function to mimic the uploading process).
import React, { useState} from "react";
const sleep = (ms, dev =1 ) => {
const msWithDev = (Math.random() * dev + 1) * ms
return new Promise(resolve => setTimeout(resolve, msWithDev))
}
function Counter() {
const [, setSortEnabled] = useState(true)
const [count, setCount] = useState(0)
const arr = [1, 2, 3, 4, 5]
const upload = async () => {
await sleep(1000)
setCount(c => c - 1)
console.log(`count state: ${count}`)
if (count === 0) {
console.warn('Sort set to enabled again.')
setSortEnabled(true)
}
}
const onClick = () => {
console.warn('Sort set to disabled.')
arr.forEach(item => {
setCount(c => c + 1)
console.error(count)
upload()
})
}
return (
<>
<button onClick={onClick}>Click me!</button>
</>
);
}
export default Counter;
The problem is in upload,
function `console.log(\`count state: ${count}\`)`
always logs 0 which means the state has never been updated.
What am I doing wrong here?
Which means the state has never been updated
It does get updated, you are just logging staled value because of closures.
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function.
To fix it, use setState functional updates:
const onClick = () => {
console.warn("Sort set to disabled.");
arr.forEach((item) => {
setCount((c) => {
console.error(c);
return c + 1;
});
upload();
});
};
Instead of relying on count, you must use Promise.all to wait for all files to be uploaded. In your case count is received from closure so even if the state is updated, the count variable remains unaffected.
You can implement the above logic with Promise.all like
function Counter() {
const [, setSortEnabled] = useState(true)
const arr = [1, 2, 3, 4, 5]
const upload = async () => {
await sleep(1000);
}
const onClick = async () => {
console.warn('Sort set to disabled.')
const promises = arr.map(item => {
console.error(count)
return upload()
});
await Promise.all(promises);
console.warn('Sort set to enabled again.')
setSortEnabled(true)
}
return (
<>
<button onClick={onClick}>Click me!</button>
</>
);
}
export default Counter;
UPDATE:
To get around the closure issue of count state, you can make use of refs and useEffect. However its a workaround and should not be preferred
import React, { useState} from "react";
const sleep = (ms, dev =1 ) => {
const msWithDev = (Math.random() * dev + 1) * ms
return new Promise(resolve => setTimeout(resolve, msWithDev))
}
function Counter() {
const [, setSortEnabled] = useState(true)
const [count, setCount] = useState(0)
const arr = [1, 2, 3, 4, 5]
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count ]);
const upload = async () => {
await sleep(1000)
setCount(c => c - 1)
console.log(`count state: ${count}`)
if (countRef.current === 0) {
console.warn('Sort set to enabled again.')
setSortEnabled(true)
}
}
const onClick = () => {
console.warn('Sort set to disabled.')
arr.forEach(item => {
setCount(c => c + 1)
upload();
})
}
return (
<>
<button onClick={onClick}>Click me!</button>
</>
);
}
export default Counter;

Textcontent not updating in react

I rewrote my class component to a functional component because i had to use react hooks. Its all working fine now except for the screen. Its not updating the text. i've added a ref to the element called screen element and the function setScreenText seems to work. I put some logs and they return what i would expect from them. If I press key 5 console.log(screenElement.textContent) returns 5 and console.log(screenElement) returns {current: div.screen, textContent: "5"} The only problem still is that its not updating the text on the screen, it stays blank.
import React, {useRef, useState} from 'react';
import '../style/App.scss';
import {UPDATE_GAME_STATE} from '../actions';
import { useDispatch } from 'react-redux';
function Keypad() {
const dispatch = useDispatch();
const KEYPAD_STATE_SUCCESS = "keypad_success";
const KEYPAD_STATE_FAILED = "keypad_failed";
const KEYPAD_STATE_INPUT = "keypad_input";
const [state, setState] = useState(KEYPAD_STATE_INPUT);
const tries = [];
const screenElement = useRef(null);
const handleKeyPress = async(value) => {
tries.push(value);
setScreenText(tries.join(''));
if (tries.length >= 4) {
const success = await tryKeyCode(tries.join(''))
const newState = {
setState: success ? KEYPAD_STATE_SUCCESS : KEYPAD_STATE_FAILED
}
const screenText = success ? "SUCCESS" : "FAILED";
const textScreenTime = success ? 3000 : 1000;
const handleGameState = success ? () => onSuccess() : () => onFailed();
setTimeout(() => {
setScreenText(screenText);
setTimeout(handleGameState, textScreenTime);
}, 200);
setState(newState);
}
}
const onSuccess = () => {
dispatch(UPDATE_GAME_STATE('completed'))
}
const onFailed = () => {
console.log("wrong code")
}
/**
* #param {string} text
*/
const setScreenText = (text) => {
screenElement.textContent = text;
console.log(screenElement.textContent); // if key 5 is pressed returns 5
console.log(screenElement); // returns {current: div.screen, textContent: "5"}
}
/**
* #param {string} code
* #returns boolean
*/
const tryKeyCode = async(code) => {
const response = (await fetch("http://localhost:3000/keypad", {
method: "POST",
body: JSON.stringify({
"code": code
}),
headers: {
"Content-Type": "application/json"
}
}));
return response.status === 200;
}
const keys = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const Key = ({number, className = ""}) => <button className={"key " + className} onClick={() => handleKeyPress(number)}>{number}</button>
const keyElements = keys.map(key => (<Key number={key}/>))
return (
<div className="keypad_wrapper">
<div className="screen" ref={screenElement}/>
{keyElements}
<Key className="last" number={0}/>
</div>
);
}
export default Keypad;
As mentioned in the react docs:
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
Thus instead of updating the screenElement.textContent you need to update screenElement.current.textContent like:
const setScreenText = (text) => {
if (screenElement.current) {
// Wait until current is available
// `current` points to the mounted element
screenElement.current.textContent = text;
console.log(screenElement.current);
}
}

change state with hooks returns Cannot read property 'filter' of undefined

For some strange reason when trying to change the array using setState (hooks) I get the following error:
Cannot read property 'filter' of undefined.
Tried console logging the array count and I am getting all the listed items back.
Clicking on the handleDelete event gives me that error.
Am I using React Hooks the wrong way?
import React, { useState } from 'react';
// Components
import NavBar from './components/navbar';
import Counters from './components/counters';
import './App.css';
const App = () => {
const [count, setCounters] = useState({
data: [
{
id: 1,
value: 4,
},
{
id: 2,
value: 0,
},
{
id: 3,
value: 0,
},
{
id: 4,
value: 0,
},
],
});
const handleIncrement = counter => {
const counters = [...count.data];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value++;
setCounters(counters);
};
const handleDecrement = counter => {
const counters = [...count.data];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value--;
setCounters(counters);
};
const handleReset = () => {
const counters = count.data.map(c => {
c.value = 0;
return c;
});
setCounters(counters);
};
const handleDelete = counterId => {
const counters = count.data.filter(c => c.id !== counterId);
setCounters(counters);
};
return (
<React.Fragment>
<NavBar
totalCounters={count.data.filter(c => c.value > 0).length}
/>
<main className="container">
<Counters
onReset={handleReset}
onDelete={handleDelete}
onIncrement={handleIncrement}
onDecrement={handleDecrement}
counters={count.data}
/>
</main>
</React.Fragment>
);
};
export default App;
The error is in handleDecrement and handleIncrement
const counters = [...count.data];
...
setCounters(counters);
In you initial state, count is an object with data property.
When you setCounters(counters), you set count as an array and after that, you try to access .data from count in the render method, but that is undefined, wich causes the error.
You need to change
setCounters(counters); // set count as an array
To
setCounters({data: counters}); // set count as an object with property 'data' as an array
Problem is you are setting the count data back incorrectly.
Instead do the following.
setCounters({ data: counters });
In some of your methods, you are setting the counters state as an array. But in the initialization what you are storing in the counters is an object with a data attribute (which is an array).
When you call the handleDelete method you are storing in the counters an array. So, when the component is re-rendered it tries to call the filter method in counters.data but counter.data does not exist at that time.
To solve the problem you can initialize the counters state so that it is an array:
const [count, setCounters] = useState([
{ id: 1, value: 4, },
{ id: 2, value: 0, },
{ id: 3,value: 0, },
{ id: 4, value: 0, },
]);
And take into account in the rest of your code that count does not have a data attribute anymore.
Another solution would be to update the count state maintaining the original structure. The handleDelete method would look like:
const handleDelete = counterId => {
const counters = count.data.filter(c => c.id !== counterId);
setCounters({ data: counters });
};

Categories

Resources