Circular dependency in useEffect - javascript

I'm trying to build a custom hook that has to alter values before using Formik's setFieldValue().
Here is how to hook looks like:
export const useCount = (tab) => {
const { values, setFieldValue } = useFormikContext();
const { count} = values;
useEffect(() => {
const handleChange = () => {
if (tab === 0) {
setFieldValue("count", count);
} else if (tab === 1) {
setFieldValue("count", count * 2);
}
};
handleChange();
}, [count]);
};
The issue at hand is that I end up with an infinite loop, due to the fact that I'm changing count inside the useEffect(). Any clues?

Replace count on tab in your useEffect, to trigger this effect only when tab is changed
export const useCount = tab => {
const {values: {count}, setFieldValue} = useFormikContext();
const handleChange = () => {
if (tab === 0) {
setFieldValue("count", count);
} else if (tab === 1) {
setFieldValue("count", count * 2);
}
}
useEffect(() => {
handleChange();
}, [tab]);
};

Related

Tensorflow JS with Chrome Dino React, passing props to Game while doing some hand pose

I'm trying a project where I use my handpose to play the dino game in chrome. It's been 6 hours and I cannot seem to find a good solution to passing props to the game. Here is the code of the App.js
function App() {
const [isTouching, setIsTouching] = useState(false);
const webcamRef = useRef(null);
const canvasRef = useRef(null);
const runHandpose = async () => {
const net = await handpose.load();
console.log('Handpose model loaded.');
// Loop and detect hands
setInterval(() => {
detect(net);
}, 100)
};
const detect = async (net) => {
if (
typeof webcamRef.current !== 'undefined' &&
webcamRef.current !== null &&
webcamRef.current.video.readyState === 4
) {
...
await handleDistance(hand);
}
}
const handleDistance = (predictions) => {
if (predictions.length > 0) {
predictions.forEach(async (prediction) => {
const landmarks = prediction.landmarks;
const thumbTipPoint = landmarks[4]
const indexFingerTipPoint = landmarks[8]
const xDiff = thumbTipPoint[0] - indexFingerTipPoint[0]
const yDiff = thumbTipPoint[1] - indexFingerTipPoint[1]
const dist = Math.sqrt(xDiff*xDiff + yDiff*yDiff)
if (dist < 35) {
setIsTouching(true);
} else {
setIsTouching(false);
}
})
}
}
useEffect(() => {
console.log(isTouching)
}, [isTouching])
useEffect(() => {
runHandpose();
}, [])
return (
<div className="App">
<div className="App-header">
<Webcam ref={webcamRef}
style={{...}}/>
<canvas ref={canvasRef}
style={{...}} />
</div>
<Game isTouching={isTouching} />
</div>
);
}
And here is some code from the game
export default function Game({ isTouching }) {
const worldRef = useRef();
const screenRef = useRef();
const groundRef1 = useRef();
const groundRef2 = useRef();
const dinoRef = useRef();
const scoreRef = useRef();
function setPixelToWorldScale() {
...
}
function handleStart() {
lastTime = null
speedScale = 1
score = 0
setupGround(groundRef1, groundRef2)
setupDino(dinoRef, isTouching)
setupCactus()
screenRef.current.classList.add("hide")
window.requestAnimationFrame(update)
}
async function update(time) {
if (lastTime == null) {
lastTime = time
window.requestAnimationFrame((time) => {
update(time)
})
return
}
const delta = time - lastTime
updateGround(delta, speedScale, groundRef1, groundRef2)
updateDino(delta, speedScale, dinoRef)
updateCactus(delta, speedScale, worldRef)
updateSpeedScale(delta)
updateScore(delta, scoreRef)
// if (checkLose()) return handleLose(dinoRef, screenRef)
lastTime = time
window.requestAnimationFrame((time) => {
update(time)
})
}
function updateSpeedScale(delta) {...}
function updateScore(delta) {...}
function checkLose() {...}
function isCollision(rect1, rect2) {...}
useEffect(() => {
console.log(isTouching)
window.requestAnimationFrame(update);
}, [isTouching])
function handleLose() {...}
useEffect(() => {
setPixelToWorldScale()
window.addEventListener("resize", setPixelToWorldScale())
document.addEventListener("click", handleStart, { once: true })
},[])
return (...);
}
What I've been trying to do is how I can pass isTouching to the game everytime my thumb and my index finger meet. But I want to avoid re-render the game and I only want to update the dino. But I cannot find a way to do that. I was also trying to create a isJump state inside the game but I don't know how to pass the setisJump to the parent (which is App) so that I can do something like this:
useEffect(() => {
setIsJump(true)
}, [isTouching])
Does anyone have a better idea of how to do this? Or did I made a mistake on passing the props here? Thank you

react context api state not persisting for synthetic onwheel event

I am trying to implement a onWheel triggered Nav for a carousel. The button nav works, while the onWheel triggers, but is somehow not accessing the initialized state of the context provider. Any insight would be appreciated. thank you.
context provider:
import CarouselContext from "../context/_carousel"
const CarouselProvider = ({ children }) => {
const [isInitialized, setIsInitialized] = useState(false)
const [carouselLength, setCarouselLength] = useState(0)
const [carouselPosition, setCarouselPosition] = useState(0)
const initializeCarousel = length => {
setCarouselLength(length)
setIsInitialized(true)
console.log(`carouselLength ${carouselLength}`)
}
const nextSlide = () => {
if (carouselPosition === carouselLength) {
setCarouselPosition(0)
} else {
setCarouselPosition(carouselPosition + 1)
}
console.log(`carouselPosition ${carouselPosition}`)
}
const previousSlide = () => {
if (carouselPosition === 0) {
setCarouselPosition(carouselLength)
} else {
setCarouselPosition(carouselPosition - 1)
}
console.log(`carouselPosition ${carouselPosition}`)
}
const state = { carouselLength, carouselPosition, isInitialized }
const methods = { initializeCarousel, nextSlide, previousSlide }
return (
<CarouselContext.Provider value={[state, methods]}>
{children}
</CarouselContext.Provider>
)
}
export default CarouselProvider
carousel structure:
return (
<Page className="works">
<CarouselProvider>
<ScrollNav>
<PreviousWorkButton />
<Carousel>
{works.map((work, index) => (
<CarouselItem key={index}>
<Work project={work} />
</CarouselItem>
))}
</Carousel>
<NextWorkButton />
</ScrollNav>
</CarouselProvider>
</Page>
)
scroll Nav (which is consoling the events are triggered, but not showing the current position of the carousel or length)
const ScrollNav = ({ children }) => {
const [, { nextSlide, previousSlide }] = useContext(CarouselContext)
const delayedScroll = useCallback(
debounce(e => changeNav(e), 500, { leading: true, trailing: false }),
[]
)
const changeNav = direction => {
if (direction === 1) {
nextSlide()
}
if (direction === -1) {
previousSlide()
}
}
const onWheel = e => {
delayedScroll(e.deltaY)
}
return <div onWheel={onWheel}>{children}</div>
}
onclick button that triggers the same event with carousel position and length persisting
const NextWorkButton = () => {
const [, { nextSlide }] = useContext(CarouselContext)
const clicked = () => {
nextSlide()
}
return (
<div className="next-work-button">
<button onClick={clicked}>
<DownArrowSvg />
</button>
</div>
)
}
edited to add console.logs in the provider as is in my local copy
console logs on click event:
carouselPosition 1
carouselLength 5
console log on wheel event (the length does not print):
carouselPosition 0
Thanks to kumarmo2 I solved this by removing the debounce and calling the event directly. I made a very hacky debounce specific to the wheel event with a timer.
my solution:
const ScrollNav = ({ children }) => {
const [, { nextSlide, previousSlide }] = useContext(CarouselContext)
const [debounce, setDebounce] = useState(false)
const timer = () => {
setTimeout(() => {
setDebounce(false)
}, 1000)
}
const changeNav = e => {
let direction = e.deltaY
if (debounce) {
return
} else if (direction >= 1) {
setDebounce(true)
timer()
nextSlide()
return
} else if (direction <= -1) {
setDebounce(true)
timer()
previousSlide()
return
}
}
return <div onWheel={changeNav}>{children}</div>
}
I think what is happening here is that, your delayedScroll is not getting updated because of useCallback. It "captures" the changeNav which in turn captures the nextSlide.
So nextSlide will be called, but since its references inside delayedScroll is not updated because of useCallback, you are facing the issue.
Can you try removing the useCallback and debounce once for delayedScroll ? and if it works, will introduce the debounce logic in the correct way.

Rendered more hooks than during the previous render React issue

I've re edited the question as it was not relevant... I got an issue in appearing in my browser when I launch my app, this issue is:
Rendered more hooks than during the previous render.
I've look all over the internet, but still don't manage to make it work.
Here is my code:
const DefaultValue = () => {
let matchingOption = options.find((option) => option.value.includes(countryLabel))
let optionSelected = options.find((option) => option.value === value)
const hasCountryLabelChanged = countryHasChanged(countryLabel)
const [selectedPathway, changeSelectedPathway] = useState(matchingOption)
useEffect(() => {
if (hasCountryLabelChanged) {
if(matchingOption) {
changeSelectedPathway(matchingOption)
} else {
changeSelectedPathway(options[0])
}
} else {
changeSelectedPathway(optionSelected)
}
},[matchingOption, optionSelected, selectedPathway, hasCountryLabelChanged])
if(selectedPathway !== undefined) {
const newLevers = levers.map((lever, index) => {
lever.value = +pathways[selectedPathway.value][index].toFixed(1) * 10
return lever
})
dispatch(Actions.updateAllLevers(newLevers))
}
return selectedPathway
}
const countryHasChanged = (countryLabel) => {
const prevValue = UsePrevious(countryLabel)
return prevValue !== countryLabel
}
const UsePrevious = (countryLabel) => {
const ref = useRef()
useEffect(() => {
ref.current = countryLabel
})
return ref.current
}
the "selectedPathway" is shown in < select value={DefaultValue} />
Your optionValueCheck call should happen inside a useEffect with one of the dependency params as countryLabel. So that whenever countryLabel updates, your function is executed.

Callback in Custom React Hooks

I have the following hooks:
function useLogin(state, url, loginMessage, callback) {
const history = useHistory();
const logged_in = state.user.authenticated;
useEffect(() => {
if (!logged_in) {history.push(url); loginMessage();}
else callback();
}, [logged_in])
return logged_in;
}
function useGroupAuth(state, url, loginMessage) {
const history = useHistory();
let has_group_auth = false;
state.user.available_teams.forEach(function(currentValue) {
if (currentValue.toString().toLowerCase() === teamname.toString().toLowerCase()) {
has_group_auth = true;
}
})
useEffect(() => {
if (!has_group_auth) {
if (state.user.available_teams.length != 0) {
history.push(url); loginMessage();
}
else
history.push("/"); loginMessage();
} else {
callback();
}
}, [has_group_auth])
return has_group_auth;
}
and they're used as
let loggedin = useLogin(state, "/accounts/login", teamhome2_message);
let properauth = useGroupAuth(state, ("/team/" + state.user.available_teams[0]), teamhome3_message);
useEffect(() => {
if (loggedin)
if (properauth)
checkteamexists(teamname);
}, []);
The problem is that, even though the code compiles, it's not behaving as I wanted it to. I only want if (properauth) to execute if loggedin is true.
My previous implementation worked because I was simply using callback without any custom hooks, as such:
useEffect(() => {
checklogin(function() {
checkauth(function() {
checkteamexists(teamname);
})
})
}, []);
How can I ensure that properauth won't execute unless loggedin is true, as described in the initial, hook-less useEffect hook?
Thanks in advance.
In your case, you can't update the useGroupAuth value. because it's returning only one value send one more variable(callback) to update/check whenever you need it. something like useState
Hook
function useGroupAuth(state, url, loginMessage) {
const history = useHistory();
const [has_group_auth, setAuth] = useState(false);
const validate = () => {
setAuth(
state.user.available_teams.some(
(currentValue) =>
currentValue.toString().toLowerCase() ===
teamname.toString().toLowerCase()
)
);
};
useEffect(validate, []);
useEffect(() => {
if (!has_group_auth) {
if (state.user.available_teams.length != 0) {
history.push(url);
loginMessage();
} else history.push("/");
loginMessage();
} else {
callback();
}
}, [has_group_auth]);
return [has_group_auth, validate];
}
Use
let [properauth, reValidate] = useGroupAuth(state, ("/team/" + state.user.available_teams[0]), teamhome3_message);
useEffect(() => {
if (loggedin){
// Do something
reValidate();
}
}, []);
It seems you are missing dependencies in your useEffect hook. Both loggedin and properauth (teamname as well, really) are referenced in the effect callback, so they should be included in the effect's dependencies.
const loggedin = useLogin(state, "/accounts/login", teamhome2_message);
const properauth = useGroupAuth(state, ("/team/" + state.user.available_teams[0]), teamhome3_message);
useEffect(() => {
if (loggedin && properauth && teamname) {
checkteamexists(teamname);
}
}, [loggedin, properauth, teamname]);

The component isn't updating when I pass in a filtered variable on a timer

So I was trying to implement a filter that is controlled by a search bar input. So I think part of the problem is that I have this filter hooked on a timer so that while the user is typing into the search bar, it isn't re-running for each letter typed in.
What it is currently doing is that after the item is typed in the search bar, the timer goes off and the filters are working but it doesn't appear that the app is re-rendering with the new filtered variable.
I suspect that it might have something to do with useEffect but I'm having trouble wrapping my head around it and it wasn't working out for whatever I was doing with it.
Here's the code:
const RecipeCards = (props) => {
const inputTypingRef = useRef(null);
let preparingElement = props.localRecipes;
let cardElement;
let elementsSorted;
const ingredientCountSort = (recipes) => {
elementsSorted = ...
}
const elementRender = (element) => {
cardElement = element.map((rec) => (
<RecipeCard
name={rec.name}
key={rec.id}
ingredients={rec.ingredients}
tags={rec.tags}
removeRecipe={() => props.onRemoveIngredients(rec.id)}
checkAvail={props.localIngredients}
/>
));
ingredientCountSort(cardElement);
};
if (inputTypingRef.current !== null) {
clearTimeout(inputTypingRef.current);
}
if (props.searchInput) {
inputTypingRef.current = setTimeout(() => {
inputTypingRef.current = null;
if (props.searchOption !== "all") {
preparingElement = props.localRecipes.filter((rec) => {
return rec[props.searchOption].includes(props.searchInput);
});
} else {
preparingElement = props.localRecipes.filter((rec) => {
return rec.includes(props.searchInput);
});
}
}, 600);
}
elementRender(preparingElement);
return (
<div className={classes.RecipeCards}>{!elementsSorted ? <BeginPrompt /> : elementsSorted}</div>
);
};
Don't worry about ingredientCountSort() function. It's a working function that just rearranges the array of JSX code.
Following up to my comment in original question. elementsSorted is changed, but it doesn't trigger a re-render because there isn't a state update.
instead of
let elementsSorted
and
elementsSorted = ...
try useState
import React, { useState } from 'react'
const RecipeCards = (props) => {
....
const [ elementsSorted, setElementsSorted ] = useState();
const ingredientCountSort = () => {
...
setElementsSorted(...whatever values elementsSorted supposed to be here)
}
Reference: https://reactjs.org/docs/hooks-state.html
I used useEffect() and an additional useRef() while restructuring the order of functions
const RecipeCards = (props) => {
//const inputTypingRef = useRef(null);
let preparingElement = props.localRecipes;
let finalElement;
const [enteredFilter, setEnteredFilter] = useState(props.searchInput);
let elementsSorted;
const [elementsFiltered, setElementsFiltered] = useState();
const refTimer = useRef();
const filterActive = useRef(false);
let cardElement;
useEffect(() => {
setEnteredFilter(props.searchInput);
console.log("updating filter");
}, [props.searchInput]);
const filterRecipes = (recipes) => {
if (enteredFilter && !filterActive.current) {
console.log("begin filtering");
if (refTimer.current !== null) {
clearTimeout(refTimer.current);
}
refTimer.current = setTimeout(() => {
refTimer.current = null;
if (props.searchOption !== "all") {
setElementsFiltered(recipes.filter((rec) => {
return rec.props[props.searchOption].includes(enteredFilter);
}))
} else {
setElementsFiltered(recipes.filter((rec) => {
return rec.props.includes(enteredFilter);
}))
}
filterActive.current = true;
console.log(elementsFiltered);
}, 600);
}else if(!enteredFilter && filterActive.current){
filterActive.current = false;
setElementsFiltered();
}
finalElement = elementsFiltered ? elementsFiltered : recipes;
};
const ingredientCountSort = (recipes) => {
console.log("sorting elements");
elementsSorted = recipes.sort((a, b) => {
...
filterRecipes(elementsSorted);
};
const elementRender = (element) => {
console.log("building JSX");
cardElement = element.map((rec) => (
<RecipeCard
name={rec.name}
key={rec.id}
ingredients={rec.ingredients}
tags={rec.tags}
removeRecipe={() => props.onRemoveIngredients(rec.id)}
checkAvail={props.localIngredients}
/>
));
ingredientCountSort(cardElement);
};
//begin render /////////////////// /// /// /// /// ///
elementRender(preparingElement);
console.log(finalElement);
return (
<div className={classes.RecipeCards}>{!finalElement[0] ? <BeginPrompt /> : finalElement}</div>
);
};
There might be redundant un-optimized code I want to remove on a brush-over in the future, but it works without continuous re-renders.

Categories