Redux useDispatch Hook on click event of nodeListOf<HTMLElement> - reactjs

I have a component that renders a grid. I'm trying to count the moves made (onclick of each grid box).
But when I include dispatch on the eventListener it returns an error. The moveCharacter function is supposed to move the character around those boxes and its working well. I just need a way to be able to count the moves made (onclick of each box) and store in general state to use in another component.
function GridBoxes():JSX.Element{
const gridValue: number = useSelector<IStateProps, IStateProps["grid"]>((state)=> state.grid);
const totalMoves: number = useSelector<IStateProps, IStateProps["totalMoves"]>((state)=> state.totalMoves);
const history = useHistory();
const dispatch = useDispatch();
useEffect(()=>{
const boxElements = document.querySelectorAll('.box');
boxElements.forEach(element => {
element.addEventListener('click', (e)=>{
moveCharacter(element.id, getCharacterPosition(boxElements));
dispatch({type: "COUNT_MOVES", totalMoves: totalMoves + 1});
console.log("moves");
});
});
});
useEffect(()=> {
let t = setInterval(()=> {
const timeSpent = document.getElementById("time-spent");
const indicator = document.getElementById("indicator");
let countDown = Number(timeSpent?.innerHTML);
countDown = countDown - 1;
let timeTakenPercent = ((gridValue*3) - countDown) / (gridValue*3) * 100;
dispatch({type:"SET_TIME", payload: (gridValue*3)-countDown});
(indicator as any).style.width = timeTakenPercent+"%";
(timeSpent as any).innerHTML = countDown.toString().length < 2 ? countDown.toString().padStart(2,"0") : countDown;
if(countDown < 1){
clearInterval(t);
history.push("/over");
play("https://freesound.org/data/previews/175/175409_1326576-lq.mp3");
}
}, 1000);
});
const [emptyBox, characterBox, foodBox]: string[] = ['<div class="box"></div>',`<div class="box"><img src=${assets.character} /></div>`, `<div class="box"><img src=${assets.food} /></div>`];
const generatedGrids: string[][] = gridPattern({grid: gridValue, box:emptyBox, character: characterBox, food: foodBox});
return (
<>
{generatedGrids.map((box, i)=> {
box = setElementId(box,i);
return <div key={i} className="col">{ReactHtmlParser(box.join(" "))}</div>;
})}
</>
);
}
export default GridBoxes;
Error gotten

The error does not really come from Redux, but from some manual DOM manipulation you are doing.
Your dispatch call only surfaces it: when calling dispatch, redux state will change which will trigger a React rerender - while you are manually fiddling around with the DOM and the two things collide.
My question is: why do you do all this? The whole point of React is that is builds the DOM for you and attaches event handlers for you. At no point should you be using something like react-html-parser, manually concatenate html strings, manually call addEventListener, modify innerHTML, style or anything else on DOM elements.
Let React build your DOM for you and this error will go away.

Related

Dots Animation Causing innerHTML Error in React Component

I am trying to create a very basic loading animation inside a react component that displays while my API is loading. I found a solution that does what I want, but once my API is loaded and the component is no longer displaying, I start getting this error:
"Uncaught TypeError: Cannot read properties of null (reading 'innerHTML')"
I have read something about clearing the interval but couldn't figure it out.
Here is the component I built:
const LoadingData = () =>{
var dots = window.setInterval(function() {
var wait = document.getElementById("wait");
if ( wait.innerHTML.length > 5 )
wait.innerHTML = "";
else
wait.innerHTML += ".";
}, 200);
return(
<p>Loading<span id="wait">.</span></p>
)
}
I also get a warning the "dots" is declared, but not used. I am not as concerned about that, but if someone could help me find a better solution I would love to hear it.
Correct way of doing it:
const LoadingData = () => {
// use state to control the dom
const [dots, addDot] = useReducer((state) => (state + 1) % 6, 1);
// set your intervals inside useEffect
useEffect(() => {
const interval = window.setInterval(() => {
addDot();
}, 200);
// remember to clear intervals
return () => clearInterval(interval);
}, [addDot]);
return (
<p>
Loading<span id="wait">{".".repeat(dots)}</span>
</p>
);
};
live example

Callback function created in custom react hook isn't being provided with an up to date value

In a custom alert system I have created in react I have 2 main functions, a custom hook:
function useAlerts(){
const [alerts, setAlerts] = useState([]);
const [count, setCount] = useState(0);
let add = function(content){
let remove = function(){
console.log(`alerts was set to ${alerts} before remove`);
setAlerts(alerts.slice(1));
};
let newAlert =
<div className = 'alert' onAnimationEnd = {remove} key = {count}>
<Warning/>
<span>
{content}
</span>
</div>
setAlerts([...alerts, newAlert]);
setCount(count + 1);
}
return [alerts,add];
}
and another element to display the data within the custom hook.
function Alerts(){
let [alerts,add] = useAlerts();
useEffect(() => {
let handler = function(){
add('test');
};
window.addEventListener('keyup',handler);
return function(){
window.removeEventListener('keyup',handler);
}
});
return (
<div className = 'alerts'>
{alerts}
</div>
)
}
the current issue I have is with the remove callback function, the console will look something like this.
let remove = function(){
console.log(`alerts was set to ${alerts} before remove`);
/// expected output: alerts was set to [<div>,<div>,<div>,<div>] before remove
/// given output: alerts was set to [] before remove
setAlerts(alerts.slice(1));
};
I understand this is because the remove function is taking the initial value of the alert state, but how do I make sure it keeps an up to date value? React doesn't seem to allow useEffect in a callback so I seem to be a bit stuck. Is passing the value in an object the way to go or something?
The issue I believe I see here is that of stale enclosures over the local React state. Use functional state updates to correctly update from the previous state instead of whatever if closed over in callback scope. In fact, use functional state updates any time the next state depends on the previous state's value. Incrementing counts and mutating arrays are two prime examples for needing functional state updates.
function useAlerts() {
const [alerts, setAlerts] = useState([]);
const [count, setCount] = useState(0);
const add = (content) => {
const remove = () => {
console.log(`alerts was set to ${alerts} before remove`);
setAlerts(alerts => alerts.slice(1));
};
const newAlert = (
<div className='alert' onAnimationEnd={remove} key={count}>
<Warning/>
<span>
{content}
</span>
</div>
);
setAlerts(alerts => [...alerts, newAlert]);
setCount(count => count + 1);
}
return [alerts, add];
}

Prevent useEffect retriggering when updating state from specific function

I've the following component:
import React, { useEffect, useState } from 'react';
const UndoInput = () => {
const [name, setName] = useState('');
return(
<div className = 'Name'>
<input
onChange = {(e) => setName(e.target.value)}
value = {name}>
</input>
<Undo
name = {name}
setName = {setName}
/>
</div>
);
}
export default UndoInput;
The <Undo/> component is a buffer storing the value of an input on every change, allowing to perform an undo action.
const Undo = ({name, setName}) => {
const [buffer, setBuffer] = useState(['', '', '']);
const [index, setIndex] = useState(-1);
useEffect(() => {
let copy = [...buffer, name].slice(-3);
let pos = index + 1;
if(pos > 2)
pos = 2;
setBuffer(copy);
setIndex(pos);
}, [name]);
const undo = () => {
let pos = index - 1;
if(pos < 0)
pos = -1;
setName(buffer[pos]);
setIndex(pos);
}
return(
<button onClick = {undo}>Undo</button>
);
}
Example:
If a user types 'abc' on the input, buffer = ['a', 'ab', 'abc'] and index = 2.
When a user clicks on the button, the component should go back to the previous state. That means, buffer = ['a', 'ab', 'abc'] and index = 1.
After clicking on the button, setName(buffer[pos]) is executed, the useEffect() is retriggered: buffer = ['ab', 'abc', 'ab'] and index = 2 (not desired).
How can I prevent to retrigger the useEffect() Hook, iff it was retriggered by the undo() function?
I wouldn't use useEffect for this. Your undo buffer is not, in my humble opinion, a side-effect in the React DOM sense, but rather internal logic you want to apply on state change. Likewise, your Undo component is a button that also happens to have reusable logic about history and state that arguably belongs more to the parent than the button itself.
Overall, I think this approach is not a clean top-down flow, and tends to lead to this kind of problems in React.
I would instead start by putting your undo logic and state in the parent:
const handleChange = function(e){
// add to undo buffer here
setName(e.target.value);
}
const handleUndo = function(){
// fetch your undo buffer here
setName(undoValue);
}
onChange = {handleChange}
onClick = {handleUndo}
After wiring up everything, you'll probably feel that you lost the clean reusability of your Undo component by doing that and you'd be right. But this is exactly what Custom Hooks are for: reusable stateful logic. There are many ways ways to go about it, but here's an example of a custom hook you could write:
const [name, setName, undoName] = useUndoableState('');
The hook would replace useState (and use it itself inside the hook), manage the buffer, and provide an additional function to call undo that rewinds and set the new name.
<input onChange={setName} />
<button onClick={undoName}>Undo</button>

Cant use values from (fetched) context(provider) as input to my useState

Short version:
I have a useEffect in a ContextProvider where I fetch some data from server. In my component I
use this Context and want to init another useState()-hook with the "incoming" data. The value in
[value, setValue] = useState(dataFromContext) does not get set/inited with dataFromContext. Why, oh why?
The very long version:
Im trying to follow (and expand on) this example of how to use context/useContext-hook in react
So, I have an AppContext and a ContextProvider, I wrap my in in index.js, everything in the example works nice.
Now I want to load data in useEffect inside my ContextProvider, so my context provider now looks like this
const AppContextProvider = ({ children }) => {
const profileInfoUrl = BASE_URL + `users/GetCurrentUsersProfileInfo/`
const {loading, getTokenSilently} = useAuth0();
const [profileInfo, setProfileInfo] = useState( {})
useEffect(() => {
if (!loading && profileInfo.user.username === '' ) {
Communication.get(profileInfoUrl, getTokenSilently, setProfileInfo);
}
},[loading, getTokenSilently])
const context = {
profileInfo,
setProfileInfo
};
return (
<AppContext.Provider value={ context }>
{children}
</AppContext.Provider>
);
}
Now, in my component, I want to display this data. One of them is users selected categories, a list of an id and isSelected, which should feed a checkboxlist.
const TsProfileSettingsPage = ( ) => {
//..
const { profileInfo, setProfileInfo } = useContext(AppContext);
const userCategories = Object.assign({}, ...profileInfo.userDetails.categories.map((c) => ({[c.id]: c})));
const [checkedCategories, setCheckedCategories] = useState(userCategories);
console.log("profileInfo.userDetails.categories : " + JSON.stringify(profileInfo.userDetails.categories));
console.log("userCategories : " + JSON.stringify(userCategories));//correct dictionary
console.log("checkedCategories : " + JSON.stringify(checkedCategories)); //empty object!
//...rest of code
So I load users selection and converts is to a dictionary that ends up in "userCategories". I use that "userCategories" to init checkedCategories. BUT, from the console.logs below, userCategories is set correctly, but checkedCategories is not, its becomes an empty object!
So my question is. Am I thinking wrong here? (obviously I am), I just want to have an external state.
Everything loads ok, but when I use it to init a useState() in my component it does not get set. I have tested to have another useEffect() in my component, but I just get the feeling that Im doing something more basic error.
Thanks for any help

useState() is not updating state from event handler?

I'm trying to recreate an old flash game in React. The object of the game is to press a button down for a certain length of time.
This is the old game:
http://www.zefrank.com/everysecond/index.html
Here is my new React implementation:
https://codesandbox.io/s/github/inspectordanno/every_second
I'm running into a problem. When the mouse is released, I calculate the amount of time between when the button was pressed and when it was released, using the Moment.js time library. If the timeDifference between the onMouseDown and onMouseUp event is within the targetTime, I want the game level to increase and the targetTime to increase as well.
I'm implementing this logic in the handleMouseUp event handler. I'm getting the expected times printed to the screen, but the logic isn't working. In addition, when I console.log() the times, they are different than the ones being printed to the screen. I'm fairly certain timeHeld and timeDifference aren't being updated correctly.
Initially I thought there was a problem with the way I was doing the event handler and I need to use useRef() or useCallback(), but after browsing a few other questions I don't understand these well enough to know if I have to use them in this situation. Since I don't need access to the previous state, I don't think I need to use them, right?
The game logic is in this wrapper component:
import React, { useState } from 'react';
import moment from 'moment';
import Button from './Button';
import Level from './Level';
import TargetTime from './TargetTime';
import TimeIndicator from './TimeIndicator';
import Tries from './Tries';
const TimerApp = () => {
const [level, setLevel] = useState(1);
const [targetTime, setTargetTime] = useState(.2);
const [isPressed, setIsPressed] = useState(false);
const [whenPressed, setPressed] = useState(moment());
const [whenReleased, setReleased] = useState(moment());
const [tries, setTries] = useState(3);
const [gameStarted, setGameStarted] = useState(false);
const [gameOver, setGameOver] = useState(false);
const timeHeld = whenReleased.diff(whenPressed) / 1000;
let timeDifference = Math.abs(targetTime - timeHeld);
timeDifference = Math.round(1000 * timeDifference) / 1000; //rounded
const handleMouseDown = () => {
!gameStarted && setGameStarted(true); //initialize game on the first click
setIsPressed(true);
setPressed(moment());
};
const handleMouseUp = () => {
setIsPressed(false);
setReleased(moment());
console.log(timeHeld);
console.log(timeDifference);
if (timeDifference <= .1) {
setLevel(level + 1);
setTargetTime(targetTime + .2);
} else if (timeDifference > .1 && tries >= 1) {
setTries(tries - 1);
}
if (tries === 1) {
setGameOver(true);
}
};
return (
<div>
<Level level={level}/>
<TargetTime targetTime={targetTime} />
<Button handleMouseDown={handleMouseDown} handleMouseUp={handleMouseUp} isGameOver={gameOver} />
<TimeIndicator timeHeld={timeHeld} timeDifference={timeDifference} isPressed={isPressed} gameStarted={gameStarted} />
<Tries tries={tries} />
{gameOver && <h1>Game Over!</h1>}
</div>
)
}
export default TimerApp;
If you want to check the whole app please refer to the sandbox.
If you update some state inside a function, and then try to use that state in the same function, it will not use the updated values. Functions snapshots the values of state when function is called and uses that throughout the function. This was not a case in class component's this.setState, but this is the case in hooks. this.setState also doesn't updates the values eagerly, but it can update while in the same function depending on a few things(which I am not qualified enough to explain).
To use updated values you need a ref. Hence use a useRef hook. [docs]
I have fixed you code you can see it here: https://codesandbox.io/s/everysecond-4uqvv?fontsize=14
It can be written in a better way but that you will have to do yourself.
Adding code in answer too for completion(with some comments to explain stuff, and suggest improvements):
import React, { useRef, useState } from "react";
import moment from "moment";
import Button from "./Button";
import Level from "./Level";
import TargetTime from "./TargetTime";
import TimeIndicator from "./TimeIndicator";
import Tries from "./Tries";
const TimerApp = () => {
const [level, setLevel] = useState(1);
const [targetTime, setTargetTime] = useState(0.2);
const [isPressed, setIsPressed] = useState(false);
const whenPressed = useRef(moment());
const whenReleased = useRef(moment());
const [tries, setTries] = useState(3);
const [gameStarted, setGameStarted] = useState(false);
const [gameOver, setGameOver] = useState(false);
const timeHeld = useRef(null); // make it a ref instead of just a variable
const timeDifference = useRef(null); // make it a ref instead of just a variable
const handleMouseDown = () => {
!gameStarted && setGameStarted(true); //initialize game on the first click
setIsPressed(true);
whenPressed.current = moment();
};
const handleMouseUp = () => {
setIsPressed(false);
whenReleased.current = moment();
timeHeld.current = whenReleased.current.diff(whenPressed.current) / 1000;
timeDifference.current = Math.abs(targetTime - timeHeld.current);
timeDifference.current = Math.round(1000 * timeDifference.current) / 1000; //rounded
console.log(timeHeld.current);
console.log(timeDifference.current);
if (timeDifference.current <= 0.1) {
setLevel(level + 1);
setTargetTime(targetTime + 0.2);
} else if (timeDifference.current > 0.1 && tries >= 1) {
setTries(tries - 1);
// consider using ref for tries as well to get rid of this weird tries === 1 and use tries.current === 0
if (tries === 1) {
setGameOver(true);
}
}
};
return (
<div>
<Level level={level} />
<TargetTime targetTime={targetTime} />
<Button
handleMouseDown={handleMouseDown}
handleMouseUp={handleMouseUp}
isGameOver={gameOver}
/>
<TimeIndicator
timeHeld={timeHeld.current}
timeDifference={timeDifference.current}
isPressed={isPressed}
gameStarted={gameStarted}
/>
<Tries tries={tries} />
{gameOver && <h1>Game Over!</h1>}
</div>
);
};
export default TimerApp;
PS: Don't use unnecessary third party libraries, especially big ones like MomentJs. They increase your bundle size significantly. Use can easily get current timestamp using vanilla js. Date.now() will give you current unix timestamp, you can subtract two timestamps to get the duration in ms.
Also you have some unnecessary state like gameOver, you can just check if tries > 0 to decide gameOver.
Similarly instead of targetTime you can just use level * .2, no need to additional state.
Also whenReleased doesn't needs to be a ref or state, it can be just a local variable in mouseup handler.
State updaters can take a value indicating the new state, or a function that maps the current state to a new state. The latter is the right tool for the job when you have state that depends on mutations of itself.
This should work if you update places in the code where you use the pattern
[value, setValue ] = useState(initial);
...
setValue(value + change);
to
[value, setValue ] = useState(initial);
...
setValue((curValue) => curValue + change);
For example,
if (timeDifference <= .1) {
setLevel((curLevel) => curLevel + 1);
setTargetTime((curTarget) => curTarget + .2);
} else if (timeDifference > .1 && tries >= 1) {
setTries((curTries) => {
const newTries = curTries - 1;
if (newTries === 1) {
setGameOver(true);
}
return newTries;
});
}
I think there are two subtle things going on here:
When you call a setState method (e.g. setRelease(moment())) the value of the associated variable (e.g. whenReleased) does not update immediately. Instead it queues a re-render, and only once that render happens will the value be updated.
The event handlers (e.g. handleMouseUp) are closures. Meaning they capture the values from the parent scope. And again therefor are only updated on a re-render. So, when handleMouseUp runs, timeDifference (and timeHeld) will be the value that was calculated during the last render.
The changes you therefore need to make are:
Move the calculation of timeDifference inside your handleMouseUp event handler.
Instead of using whenReleased in your timeDifference calculation, you need to use a local variable set to moment() (You can also set whenReleased via setReleased, but that value won't be available to you inside your event handler).
const handleMouseUp = () => {
const released = moment();
setIsPressed(false);
setReleased(released);
const timeHeld = released.diff(whenPressed) / 1000;
const timeDifference = Math.round(1000 * Math.abs(targetTime - timeHeld)) / 1000;
console.log(timeHeld);
console.log(timeDifference);
if (timeDifference <= .1) {
setLevel(level + 1);
setTargetTime(targetTime + .2);
} else if (timeDifference > .1 && tries >= 1) {
setTries(tries - 1);
}
if (tries === 1) {
setGameOver(true);
}
};
When the mouse is released, I calculate the amount of time between when the button was pressed and when it was released...
This is not true ... but can be ... just move the time difference calulations into handleMouseUp()
... also - you don't need whenReleased

Resources