Problem in game lifecycle with React-Hooks - reactjs

Task:
There is a field (grid) along which the snake's head moves, when you click on the arrows, the head changes direction.
Rendering should take place according to the initial speed (and it can also change depending on conditions, it is not included in the context of this request), the speed is calculated in seconds / milliseconds.
All code works fine except for the problem (GameSleep method) with managing this same render delay until the next step (render).
Who can help with setting up a lifecycle that suits the original task?
main screen
p.s. Sorry for the long listing, could not hide in the spoiler
CODE
Index.js:
ReactDOM.render(<Provider store={store}><Snake/></Provider>, document.getElementById('root'));
Snake.js:
const Snake = (props) => {
const [process, setProcess] = useReducer(
ReducerProcess, {start: false, victory: false, gameover: false, reset: true }
);
const [direction, setDirection] = useState(undefined);
const SnakeEventListener = useCallback((event) => {
if( checkKey(event.code) ) {
const direction = getDirection(event.code);
setDirection(direction);
}
}, []);
const onProcess = useCallback((inType) => {
switch(inType) {
case STATE.reset:
setDirection(undefined); break;
case STATE.start:
document.addEventListener('keydown', SnakeEventListener, false); break;
case STATE.victory:
document.removeEventListener('keydown', SnakeEventListener, false); break;
case STATE.gameover:
document.removeEventListener('keydown', SnakeEventListener, false); break;
default: break;
}
setProcess({type: inType});
}, [SnakeEventListener]);
return (
<div>
<Grid process={process} toProcess={onProcess} direction={direction} toDirection={setDirection}/>
<button onClick={()=>onProcess('reset')}>Reset</button>
<button onClick={()=>onProcess('start')}>Start</button>
</div>
);
}
Grid.js:
const Grid = (props) => {
const {process, toProcess, direction, toDirection} = props;
const [snake, setSnake] = useState(undefined);
const [grid, setGrid] = useState(undefined);
// Effect => Initial
useEffect(() => {
const Initial = () => {
setSnake( initSnake() );
toDirection( initDirection() );
setGrid( InitGrid() );
}
if(process.reset && !direction) { Initial(); }
}, [process, direction, toDirection]);
// Effect => Game Loop
useEffect(() => {
const GameLoop = () => {
const build = BuildGrid(snake, direction);
if(build.victory || build.gameover) {
if(build.victory) { toProcess('victory'); }
if(build.gameover) { toProcess('gameover'); }
}else{
setSnake(build.snake);
setGrid(build.grid);
}
}
if(process.start && direction) { GameLoop(); }
}, [process, toProcess, direction, snake]);
return (<div>{ Cells(grid) }</div>);
}
Building.js:
const BuildGrid = (input) => {
const content = Execute(input);
return content;
}
const Execute = async (input) => {
await GameSleep(input.speed);
let newSnake = input.snake;
… … …
return { snake: newSnake, victory: victory, gameover: gameover };
}
const GameSleep = async (delay) => {
return new Promise(resolve => setTimeout(resolve, delay));
}

You can try a clearTimeout(timeoutName). You can create a function that clears the timeout.
let gameSleepTimeout;
const GameSleep = (delay) => {
gameSleepTimeout = setTimeout(() => console.log('setTimeout callback'), delay);
};
const clearGameSleep = () => clearTimeout(gameSleepTimeout);

Related

react-countdown is not reseting or re-rendering second time

What I am trying to do is to update the reset the countdown after changing the status.
There are three status that i am fetching from API .. future, live and expired
If API is returning future with a timestamp, this timestamp is the start_time of the auction, but if the status is live then the timestamp is the end_time of the auction.
So in the following code I am calling api in useEffect to fetch initial data pass to the Countdown and it works, but on 1st complete in handleRenderer i am checking its status and updating the auctionStatus while useEffect is checking the updates to recall API for new timestamp .. so far its working and 2nd timestamp showed up but it is stopped ... means not counting down time for 2nd time.
import React, { useEffect } from 'react';
import { atom, useAtom } from 'jotai';
import { startTimeAtom, auctionStatusAtom } from '../../atoms';
import { toLocalDateTime } from '../../utility';
import Countdown from 'react-countdown';
import { getCurrentAuctionStatus } from '../../services/api';
async function getAuctionStatus() {
let response = await getCurrentAuctionStatus(WpaReactUi.auction_id);
return await response.payload();
}
const Counter = () => {
// component states
const [startTime, setStartTime] = useAtom(startTimeAtom);
const [auctionStatus, setAuctionStatus] = useAtom(auctionStatusAtom);
useEffect(() => {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
setStartTime(toLocalDateTime(response.end_time, WpaReactUi.time_zone));
});
}, [auctionStatus]);
//
const handleRenderer = ({ completed, formatted }) => {
if (completed) {
console.log("auction status now is:", auctionStatus);
setTimeout(() => {
if (auctionStatus === 'future') {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
});
}
}, 2000)
}
return Object.keys(formatted).map((key) => {
return (
<div key={`${key}`} className={`countDown bordered ${key}-box`}>
<span className={`num item ${key}`}>{formatted[key]}</span>
<span>{key}</span>
</div>
);
});
};
console.log('starttime now:', startTime);
return (
startTime && (
<div className="bidAuctionCounterContainer">
<div className="bidAuctionCounterInner">
<Countdown
key={auctionStatus}
autoStart={true}
id="bidAuctioncounter"
date={startTime}
intervalDelay={0}
precision={3}
renderer={handleRenderer}
/>
</div>
</div>
)
);
};
export default Counter;
You use auctionStatus as a dependency for useEffect.
And when response.status is the same, the auctionStatus doesn't change, so your useEffect won't be called again.
For answering your comment on how to resolve the issue..
I am not sure of your logic but I'll explain by this simple example.
export function App() {
// set state to 'live' by default
const [auctionStatus, setAuctionStatus] = React.useState("live")
React.useEffect(() => {
console.log('hello')
changeState()
}, [auctionStatus])
function changeState() {
// This line won't result in calling your useEffect
// setAuctionStatus("live") // 'hello' will be printed one time only.
// You need to use a state value that won't be similar to the previous one.
setAuctionStatus("inactive") // useEffect will be called and 'hello' will be printed twice.
}
}
You can simply use a flag instead that will keep on changing from true to false like this:
const [flag, setFlag] = React.useState(true)
useEffect(() => {
// ..
}, [flag])
// And in handleRenderer
getAuctionStatus().then((response) => {
setFlag(!flag);
});
Have a look at the following useCountdown hook:
https://codepen.io/AdamMorsi/pen/eYMpxOQ
const DEFAULT_TIME_IN_SECONDS = 60;
const useCountdown = ({ initialCounter, callback }) => {
const _initialCounter = initialCounter ?? DEFAULT_TIME_IN_SECONDS,
[resume, setResume] = useState(0),
[counter, setCounter] = useState(_initialCounter),
initial = useRef(_initialCounter),
intervalRef = useRef(null),
[isPause, setIsPause] = useState(false),
isStopBtnDisabled = counter === 0,
isPauseBtnDisabled = isPause || counter === 0,
isResumeBtnDisabled = !isPause;
const stopCounter = useCallback(() => {
clearInterval(intervalRef.current);
setCounter(0);
setIsPause(false);
}, []);
const startCounter = useCallback(
(seconds = initial.current) => {
intervalRef.current = setInterval(() => {
const newCounter = seconds--;
if (newCounter >= 0) {
setCounter(newCounter);
callback && callback(newCounter);
} else {
stopCounter();
}
}, 1000);
},
[stopCounter]
);
const pauseCounter = () => {
setResume(counter);
setIsPause(true);
clearInterval(intervalRef.current);
};
const resumeCounter = () => {
setResume(0);
setIsPause(false);
};
const resetCounter = useCallback(() => {
if (intervalRef.current) {
stopCounter();
}
setCounter(initial.current);
startCounter(initial.current - 1);
}, [startCounter, stopCounter]);
useEffect(() => {
resetCounter();
}, [resetCounter]);
useEffect(() => {
return () => {
stopCounter();
};
}, [stopCounter]);
return [
counter,
resetCounter,
stopCounter,
pauseCounter,
resumeCounter,
isStopBtnDisabled,
isPauseBtnDisabled,
isResumeBtnDisabled,
];
};

How to solve a situation when a component calls setState inside useEffect but the dependencies changes on every render?

I have this component:
const updateUrl = (url: string) => history.replaceState(null, '', url);
// TODO: Rename this one to account transactions ATT: #dmuneras
const AccountStatement: FC = () => {
const location = useLocation();
const navigate = useNavigate();
const { virtual_account_number: accountNumber, '*': transactionPath } =
useParams();
const [pagination, setPagination] = useState<PaginatorProps>();
const [goingToInvidualTransaction, setGoingToInvidualTransaction] =
useState<boolean>(false);
const SINGLE_TRANSACTION_PATH_PREFIX = 'transactions/';
// TODO: This one feels fragile, just respecting what I found, but, we could
// investigate if we can jsut rely on the normal routing. ATT. #dmuneras
const transactionId = transactionPath?.replace(
SINGLE_TRANSACTION_PATH_PREFIX,
''
);
const isFirst = useIsFirstRender();
useEffect(() => {
setGoingToInvidualTransaction(!!transactionId);
}, [isFirst]);
const {
state,
queryParams,
dispatch,
reset,
setCursorAfter,
setCursorBefore
} = useLocalState({
cursorAfter: transactionId,
includeCursor: !!transactionId
});
const {
filters,
queryParams: globalQueryParams,
setDateRange
} = useGlobalFilters();
useUpdateEffect(() => {
updateUrl(
`${location.pathname}?${prepareSearchParams(location.search, {
...queryParams,
...globalQueryParams
}).toString()}`
);
}, [transactionId, queryParams]);
useUpdateEffect(() => dispatch(reset()), [globalQueryParams]);
const account_number = accountNumber;
const requestParams = accountsStateToParams({
account_number,
...state,
...filters
});
const { data, isFetching, error, isSuccess } =
useFetchAccountStatementQuery(requestParams);
const virtualAccountTransactions = data && data.data ? data.data : [];
const nextPage = () => {
dispatch(setCursorAfter(data.meta.cursor_next));
};
const prevPage = () => {
dispatch(setCursorBefore(data.meta.cursor_prev));
};
const onRowClick = (_event: React.MouseEvent<HTMLElement>, rowData: any) => {
if (rowData.reference) {
if (rowData.id == transactionId) {
navigate('.');
} else {
const queryParams = prepareSearchParams('', {
reference: rowData.reference,
type: rowData.entry_type,
...globalQueryParams
});
navigate(
`${SINGLE_TRANSACTION_PATH_PREFIX}${rowData.id}?${queryParams}`
);
}
}
};
const checkIfDisabled = (rowData: TransactionData): boolean => {
return !rowData.reference;
};
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [data?.meta]);
const showTransactionsTable: boolean =
Array.isArray(virtualAccountTransactions) && isSuccess && data?.data;
const onTransactionSourceLoaded = (
transactionSourceData: PayoutDetailData
) => {
const isIncludedInPage: boolean = virtualAccountTransactions.some(
(transaction: TransactionData) => {
if (transactionId) {
return transaction.id === parseInt(transactionId, 10);
}
return false;
}
);
if (!goingToInvidualTransaction || isIncludedInPage) {
return;
}
const fromDate = dayjs(transactionSourceData.timestamp);
const toDate = fromDate.clone().add(30, 'day');
setDateRange({
type: 'custom',
to: toDate.format(dateFormat),
from: fromDate.format(dateFormat)
});
setGoingToInvidualTransaction(false);
};
const fromDate = requestParams.created_after || dayjs().format('YYYY-MM-DD');
const toDate = requestParams.created_before || dayjs().format('YYYY-MM-DD');
const routes = [
{
index: true,
element: (
<BalanceWidget
virtualAccountNumber={account_number}
fromDate={fromDate}
toDate={toDate}
/>
)
},
{
path: `${SINGLE_TRANSACTION_PATH_PREFIX}:transaction_id`,
element: (
<TransactionDetails
onTransactionSourceLoaded={onTransactionSourceLoaded}
/>
)
}
];
return (........
I get this error: Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
The useEffect where the issue is, it is this one:
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [data?.meta]);
Considering previous answers, would the solution be to make sure I return a new object each time? But I am not sure what would be the best approach. Any clues ?
did you want the useEffect to start every changes of 'data?.meta' ?
Without reading all the code, I believe the data.meta object changes on every render. There is a way to change the useEffect to narrow done its execution conditions:
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [!data?.meta, data?.meta?.has_previous_page, data?.meta?.has_next_page]);
Please note the ! before data.?.meta which makes the hook test only for presence or absence of the object, since your code doesn't need more than that information.

Update react button disabled state on redux state change

I've set up a new project with React, Redux (using toolkit). I've got a button that needs to be disabled if the user does not have enough of a particular resource. I've confirmed that the state is being updated properly and the reducers are applying to state properly, but I am unable to get the button to disable when that resource falls below the supplied price.
I've tried duplicating state from redux using a useState hook, but setting the state within canAfford() still doesn't disable the button. I'm at a bit of a loss, and feel like I'm just missing something about redux state and rendering.
Here's the button component I'm working with:
function BuyBtn({ technology, label, resourceType, price, requirements = []}: IBuyBtn) {
const units = useSelector((state: any) => state.units);
const tech = useSelector((state: any) => state.tech);
const resources = useSelector((state: any) => state.resources);
const dispatch = useDispatch();
let disabled = false;
let unlocked = true;
useEffect(() => {
disabled = !canAfford()
}, [resources])
const canAfford = (): boolean => {
console.log('Units:', units);
console.log("Checking affordability");
if (resourceType.length != price.length) {
throw `BuyBtn Error: price length is ${price.length} but resource length is ${resourceType.length}.`;
}
resourceType.forEach((res, i) => {
const resPrice = price[i];
if (resources[res] < resPrice) {
return false;
}
});
return true;
};
const meetsRequirements = (): boolean => {
if (requirements.length === 0) {
return true;
}
requirements.forEach((req) => {
if (!tech[req]) {
return false;
}
});
return true;
};
const buyThing = () => {
if (canAfford() && meetsRequirements()) {
resourceType.forEach((res, i) => {
const resPrice = price[i];
dispatch(SubtractResource(res, resPrice));
});
dispatch(UnlockTech(technology, true))
}
};
if (meetsRequirements() && canAfford()) {
return (
<button onClick={buyThing} disabled={disabled}>{label}</button>
);
} else {
return null;
}
}
export default BuyBtn;
Instead of using disabled as variable make it State which will trigger re-render:
function BuyBtn({ technology, label, resourceType, price, requirements = []}: IBuyBtn) {
const units = useSelector((state: any) => state.units);
const tech = useSelector((state: any) => state.tech);
const resources = useSelector((state: any) => state.resources);
const dispatch = useDispatch();
const [disabled, setDisabled] = React.useState(false);
let unlocked = true;
const canAfford = (): boolean => {
console.log('Units:', units);
console.log("Checking affordability");
if (resourceType.length != price.length) {
throw `BuyBtn Error: price length is ${price.length} but resource length is ${resourceType.length}.`;
}
let isAffordable = true
resourceType.forEach((res, i) => {
const resPrice = price[i];
if (resources[res] < resPrice) {
isAffordable = false;
}
});
return isAffordable;
};
useEffect(async() => {
const value = await canAfford();
setDisabled(!value);
}, [resources])
const meetsRequirements = (): boolean => {
if (requirements.length === 0) {
return true;
}
let isMeetingRequirements = true;
requirements.forEach((req) => {
if (!tech[req]) {
isMeetingRequirements = false;
}
});
return isMeetingRequirements;
};
const buyThing = () => {
if (canAfford() && meetsRequirements()) {
resourceType.forEach((res, i) => {
const resPrice = price[i];
dispatch(SubtractResource(res, resPrice));
});
dispatch(UnlockTech(technology, true))
}
};
if (meetsRequirements() && canAfford()) {
return (
<button onClick={buyThing} disabled={disabled}>{label}</button>
);
} else {
return null;
}
}
export default BuyBtn;

Updating React state in nested setTimeout callbacks

Can someone please tell me what's wrong with this and why the state of the 'video variable' remains false? So, even after the h2 element has rendered and is visible (i.e. the state of the video variable has been updated to true), when I click and call the hideVideo function, the video state remains false? Many thanks.
export default function App() {
const [message, showMessage] = useState(false);
const [video, setVideo] = useState(false);
let modalTimeout, videoTimeout;
useEffect(() => {
window.addEventListener("click", hideVideo);
setupTimeouts();
return () => {
clearTimeout(modalTimeout);
clearTimeout(videoTimeout);
};
}, []);
const setupTimeouts = () => {
modalTimeout = setTimeout(() => {
showMessage(true);
videoTimeout = setTimeout(() => {
showMessage(false);
setVideo(true);
}, 4000);
}, 2000);
};
const hideVideo = () => {
console.log(video);
showMessage(false);
if (video === true) {
setVideo(false);
}
};
return (
<div className="App">
{message && <h1>Message</h1>}
{video && <h2>Video</h2>}
</div>
);
}
When you call useEffect the window listener attach the default video value that is false to the function hideVideo() so it will be always false, I created a button to show you that the video state value does change. check the last test function
export default function App() {
const [message, showMessage] = useState(false);
const [video, setVideo] = useState(false);
let modalTimeout, videoTimeout;
useEffect(() => {
window.addEventListener("click", hideVideo);
setupTimeouts();
return () => {
clearTimeout(modalTimeout);
clearTimeout(videoTimeout);
};
}, []);
const setupTimeouts = () => {
modalTimeout = setTimeout(() => {
showMessage(true);
videoTimeout = setTimeout(() => {
showMessage(false);
setVideo(true);
}, 4000);
}, 2000);
};
const hideVideo = () => {
console.log(video);
showMessage(false);
if (video) {
setVideo(false);
}
};
const test = (event) => {
event.stopPropagation();
console.log(video)
}
return (
<>
{message && <h1>Message</h1>}
{video && <h2>Video</h2>}
<button onClick={test}>test</button>
</>
);
}

useState not updating the state

I'm trying to filter users with input search inside the function searchByName.
I manage to get the right result in copyUsersvariable but unfortunately it does not reflect the change inside the state.
Forgot to mention, using bare React-App with hooks and typescript.
For example, i write 'p' in the input and recieve the right filtered array in copyUsers variable but then i push it into the state it does not update.
Attaching screenshot for understanding the situation better:
what i have tried instead setFilteredUsers(copyUsers):
setFilteredUsers(() => [...filteredUsers, copyUsers]);
setFilteredUsers(() => copyUsers);
main component:
const { value } = useSelector(({ test }: any) => test);
const [users, setUsers] = useState<Users>([]);
const [filteredUsers, setFilteredUsers] = useState<Users>([]);
const [searchNameValue, setSearchNameValue] = useState<string>("");
const [selectedUser, setSelectedUser] = useState<User>();
const [searchOrderBy, setSearchOrderBy] = useState<string>("");
const dispatch = useDispatch();
useEffect(() => {
const get = async () => {
const response = await ApiTest.testGet();
setUsers(response);
setSearchOrderBy("desc");
};
get();
}, []);
useEffect(() => {
searchByName();
setNewOrder();
}, [searchOrderBy]);
useEffect(() => {
console.log('search value changed!', searchNameValue);
searchByName();
setNewOrder()
}, [searchNameValue]);
const setNewOrder = () => {
if (users.length) {
let copyUsers = JSON.parse(
JSON.stringify(filteredUsers.length ? filteredUsers : users)
);
switch (searchOrderBy) {
case "desc":
copyUsers.sort((a: any, b: any) => {
if (a.id > b.id) {
return -1;
}
if (b.id > a.id) {
return 1;
}
return 0;
});
break;
case "asc":
copyUsers.sort((a: any, b: any) => {
if (b.id > a.id) {
return -1;
}
if (a.id > b.id) {
return 1;
}
return 0;
});
break;
default:
break;
}
filteredUsers.length ? setFilteredUsers(copyUsers) : setUsers(copyUsers);
}
};
const searchByName = () => {
if (searchNameValue) {
let copyUsers = JSON.parse(JSON.stringify(users));
copyUsers = copyUsers.filter((user: User) => {
return user.name
.toLocaleLowerCase()
.includes(searchNameValue.toLocaleLowerCase());
});
console.log("copyUsers =", copyUsers);
setFilteredUsers(copyUsers);
console.log("filteredUsers =", filteredUsers);
}
};
const UserCards =
!!users.length &&
(searchNameValue ? filteredUsers : users).map(user => {
return (
<UserCard
selectedUser={selectedUser}
setSelectedUser={(user: User) => setSelectedUser(user)}
user={user}
/>
);
});
return (
<div>
<FilterBar
searchOrderBy={searchOrderBy}
searchSetOrderBy={(value: string) => setSearchOrderBy(value)}
setSearchNameValue={(value: string) => setSearchNameValue(value)}
searchNameValue={searchNameValue}
/>
<div style={{ display: "flex", flexFlow: "wrap" }}>{UserCards}</div>
</div>
);

Resources