I am trying to figure out how to make eslint with the rule react-hooks/exhaustive-deps happy and also use the build in Animated library in react native to make animations with useEffect.
The following code should highlight the button when clicked, by overlaying it with a colored view.
const Component = props => {
const [active, setActive] = useState(false);
const [opacity, setOpacity] = useState(new Animated.Value(0));
useEffect(() => {
if (active) {
Animated.timing(opacity, {
toValue: 1,
duration: 200,
useNativeDriver: true
}).start();
} else {
setOpacity(new Animated.Value(0))
}
}, [active, opacity]); // <- Works fine without `opacity`, but eslint wants this
return (
<View>
<Animated.View style={{backgroundColor: "blue", opacity}} />
<TouchableOpacity onPress={()=> setActive(!active)} />
</View>
)
};
Is there any way of doing this (with useCallback, useMemo, etc.) without disabling the rule?
You don't need to call setOpacity, instead you can use setValue:
opacity.setValue(0)
There is also no need to add opacity to dependencies, because it never changes. ESLint doesn't always get it right.
Related
i am building a nextjs component called SocialMediaIcon = this component should recive a single lottie json path and render a lottie svg, when the user hovers on the animation it should start playing and when the mouse leaves it should reset the animation to position 0 ,
import { useEffect, useRef, useState } from 'react'
import { LottiePlayer } from 'lottie-web'
function SocialMediaIcon({ iconPath }) {
const ref = useRef(null)
const anim = useRef(null)
const [lottie, setLottie] = useState(null)
useEffect(() => {
import('lottie-web').then((Lottie) => setLottie(Lottie.default))
}, [])
useEffect(() => {
if (lottie && ref.current) {
anim.current = lottie.loadAnimation({
container: ref.current,
renderer: 'svg',
loop: true,
autoplay: false,
// path to your animation file, place it inside public folder
animationData: iconPath,
})
return () => anim.current.destroy()
}
}, [lottie])
const handleAnimation = () => {
lottie.play()
}
const handlemouseOut = () => {
lottie.goToAndStop(0)
}
return (
<div
ref={ref}
className="lottie h-20 rounded-lg bg-white p-2"
onMouseEnter={handleAnimation}
onMouseLeave={handlemouseOut}
></div>
)
}
export default SocialMediaIcon
and this is how i am rendering the component in the parrent component :
<SocialMediaIcon iconPath={githubJson} />
<SocialMediaIcon iconPath={twitterJson} />
the problem i'm having is that when i hover over one component all the components are playing the animation instead of just the one that has been hovered
i have already tried :
1.using a anim = useRef() hook and passing it to lottie.play(anim.current) which resulted in no animation being played at all
2.i also tried passing animation to lottie play lottie.play(lottie.animation) thinking every instance has its own animation and the animation did not play .
it would be greatly appreciated if you could help me understand this on a deeper level, i think im really missing a important concept not sure if its about lottie or nextjs in genral.
somehow I got an infinity loop, the weird situation that I did solve it but I got a warning and I wish to fix the warning.
this is code that work:
import { ArrowDropDown, ArrowRight } from "#material-ui/icons";
import React, { useState, useEffect, useCallback } from "react";
import "./tree.css";
const Tree = ({ explorer }) => {
const [expand, setExpand] = useState(true);
const [arrow, setArrow] = useState(false);
const stateHandler = () => {
setExpand(!expand);
setArrow(!arrow);
};
useEffect(() => {
//this function will display only the first Tree as init the page.
stateHandler();
}, []);
return (
<div>
<div className="treeInfo">
{arrow ? (
<ArrowDropDown className="treeIcon" />
) : (
<ArrowRight className="treeIcon" />
)}
<span className="treeTitle" onClick={stateHandler}>
{explorer.name}
</span>
</div>
<div
style={{
display: expand ? "block" : "none",
paddingLeft: 20,
cursor: "pointer",
}}
>
{explorer.items.map((explore) => {
return <Tree key={explore.id} explorer={explore} />;
})}
{/* {explorer.items.map((explore) => (
<Tree explorer={explore} />
))} */}
</div>
</div>
);
};
export default Tree;
and this is the warning:
src\components\Tree\Tree.js
Line 17:6: React Hook useEffect has a missing dependency: 'stateHandler'. Either include it or remove the dependency array react-hooks/exhaustive-deps
Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.
WARNING in src\components\Tree\Tree.js
Line 17:6: React Hook useEffect has a missing dependency: 'stateHandler'. Either include it or remove the dependency array react-hooks/exhaustive-deps
webpack compiled with 1 warning
**
The result : (it's good its what I want to display a tree view of
files)
**
well as I can read I got this warning because useEffect got not dependencies, when I add the stateHandler dependencies I got an infinity loop, so I add a callback function but it still doesn't solve the infinity loop.
this is the code with the useCallback (its the same code, just with useCallback and a bit of configure of the useEffect):
const initTree = useCallback(() => {
setExpand(!expand);
setArrow(!arrow);
}, [setExpand, setArrow, arrow, expand]);
useEffect(() => {
//this function will display only the first Tree as init the page.
initTree();
}, [initTree]);
You need to change this:
const initTree = useCallback(() => {
setExpand(!expand);
setArrow(!arrow);
}, [setExpand, setArrow, arrow, expand]);
to this:
const initTree = useCallback(() => {
setExpand(e => !e);
setArrow(a => !a);
}, []);
Otherwise what happens is this:
The component renders, initTree variable is initialized with a function, the Effect is run and setExpand and setArrow are called.
That triggers a new render
The useCallback hook checks if the deps of initTree have changed and yes, arrow and expand have changed indeed, hence initTree variable is updated with a new function
The effect checks if initTree has changed from the previous render, and yes, it has changed, hence the effect executes again calling initTree again.
There you are stuck in an infinite render loop.
Eslint shouldn't complain of missing deps if you don't put the setState in deps, since they do not change during renders, unless you are passing them through props.
If you are ok with having another state, you can simply set something like a loading state and make the useEffect depend on that
State would look like this:
const [expand, setExpand] = useState(true);
const [arrow, setArrow] = useState(false);
const [loading, setLoading] = useState(true);
And then based on this, useEffect would change as follows:
useEffect(() => {
//this function will display only the first Tree as init the page.
stateHandler();
setLoading(false);
}, [loading]);
Once loading is set to false, it's value is not going to change and the useEffect will not be triggered indefinitely
I have a framer motion component (I've tried with Stitches components too) that animates to and from view based on a useState variable:
const [visible, setVisible] = useState(true);
<motion.div
animate={visible ? "one" : "two"}
variants={{
one: {
transform: "translateX(0%)",
opacity: 1
},
two: {
transform: "translateX(100%)",
opacity: 0
}
}}
>
// children
</motion.div>
The children components are wrapped in contexts, and the problem is that their states reset every time the animation is triggered.
I've tried declaring the animation component outside of its current scope. In its current scope it resets the states of everything except the "currentUser" state in my AuthProvider context. Outside the scope it resets "currentUser" too. I've decided not to include the original code for brevity.
Here is a minimum reproducible. If you type something in the input, and then click the toggle button, the input is lost.
DummyInputComponent should be stateless, use the value provided by the context and also have a onChange handler function passed on from the context
Something like this:
export function AuthProvider({ children }) {
const [dummyValue, setDummyValue] = useState(null);
const handlechange = (e) => {
setDummyValue(e.target.value);
};
const value = {
dummyValue,
onChange: handlechange
};
const DummyInputComponent = () => {
return <input value={value.dummyValue || ""} onChange={value.onChange} />;
};
return (
<AuthContext.Provider value={value}>
<DummyInputComponent />
</AuthContext.Provider>
);
}
Working CodeSandbox
I know the common use case for this is for performance tweaking, but I ended up avoiding the rerenders caused by framer motion (supposedly) by using memoization:
const Memoized = React.memo(({ children }) => {
const [visible, setVisible] = useState(true);
return (
<motion.div
animate={visible ? "one" : "two"}
variants={{
one: {
transform: "translateX(0%)",
opacity: 1
},
two: {
transform: "translateX(100%)",
opacity: 0
}
}}
>
{children}
</motion.div>
);
});
I don't know if this is a good way to do it, so I'll leave this open.
I'm using NextJs to take advantage of Server Side Rendering. And also I have a navbar in my application that should change styles with scroll position. How can I check whether the window has been scrolled more than 100px, on my NextJs application?
You can simply use a useEffect hook like this:
import { useEffect, useState } from "react";
const IndexPage = () => {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
// just trigger this so that the initial state
// is updated as soon as the component is mounted
// related: https://stackoverflow.com/a/63408216
handleScroll();
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div style={{ height: 4000 }}> {/* just added to make scrollbar available */}
<div style={{ position: "fixed", top: 0 }}>
{scrollY > 100
? "Scrolled more than 100px"
: "Still somewhere near the top!"}
</div>
</div>
);
};
export default IndexPage;
Sandbox: https://codesandbox.io/s/cocky-drake-1xe0g
This code can be further optimized by debouncing the scroll handler. And optionally, setting the state only if it is not changed (not sure if newer versions of React handle this itself).
Your question was directly related to this thread: https://stackoverflow.com/a/59403018/11613622
For debouncing/throttling you can refer this: How to use throttle or debounce with React Hook?
Also if you don't want to use the provided solutions in that thread simply wrap the handleScroll with _.debounce and then feed it to the event handler.
with react-native, I want to use componentWillMount without using a class
await Font.loadAsync({
gotham_medium: require("../../assets/GothamMedium_1.ttf")
});
}
const Button = (props: TouchableOpacityProps & ButtonProps) => (
<TouchableOpacity {...props} style={styles.button}>
<Text style={styles.title}>{props.title}</Text>
</TouchableOpacity>
);
export default Button;
But I have a problem on the device :
error on the device
It says the problem is on this line (and it is):
async componentWillMount = () => {
When you use an async function, the async keyword goes right before () => (a vanilla js syntax error). Like this:
componentWillMount = async () => {
But, that's not the main problem. When not using a class, you need the useEffect hook.
So, try something like this (the whole component, and deleting componentWillMount):
const Button = (props: TouchableOpacityProps & ButtonProps) => {
useEffect(async () => {
await Font.loadAsync({
gotham_medium: require("../../assets/GothamMedium_1.ttf")
});
}, []);
return (
<TouchableOpacity {...props} style={styles.button}>
<Text style={styles.title}>{props.title}</Text>
</TouchableOpacity>
);
};
And at the top of the file:
import { useEffect } from 'react';
You can use Hooks for this,
from the docs,
If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
And
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.
useEffect(async () => {
await Font.loadAsync({
gotham_medium: require("../../assets/GothamMedium_1.ttf")
});
},[]);