I am using a function (getActorInfo()) in react to grab info from an api and set that in a State. It works but the function wont stop running.
export default function ActorProfile({ name, img, list, id, getActorInfo }) {
const [showList, setShowList] = useState(false);
const [actorInfo, setActorInfo] = useState({});
getActorInfo(id).then(val => setActorInfo(val));
console.log(actorInfo)
return (
<Wrapper>
<Actor
id={id}
name={name}
img={img}
onClick={() => {
setShowList(!showList);
}}
actorBirthday={actorInfo.actorBirthday}
/>
{showList && <MovieList list={list} actorInfo={actorInfo} />}
</Wrapper>
);
}
I tried using useEffect like this
useEffect(() => {
getActorInfo(id).then(val => setActorInfo(val));
}, {});
But I get an error that I do not understand
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in ActorProfile (at App.js:60)
My question is how to have this function only run once?
Anything in a functional component body will run every render. Changing to a useEffect is the correct solution to this problem.
It isn't working for you because useEffect takes an array as its second parameter, not an object. Change it to [], and it will only run once.
useEffect(() => {
getActorInfo(id).then(val => setActorInfo(val));
}, []);
This will be equivalent to the class-based componentDidMount.
If your hook has a dependency, you add it to the array. Then the effect will check to see if anything in your dependency array has changed, and only run the hook if it has.
useEffect(() => {
// You may want to check that id is truthy first
if (id) {
getActorInfo(id).then(val => setActorInfo(val));
}
}, [id]);
The resulting effect will be run anytime id changes, and will only call getActorInfo if id is truthy. This is an equivalent to the class-based componentDidMount and componentDidUpdate.
You can read more about the useEffect hook here.
You are still not checking if the component is mounted before you set the state. You can use a custom hook for that:
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
Then in your component you can do:
const isMounted = useIsMounted();
useEffect(() => {
getActorInfo(id).then(
val => isMounted && setActorInfo(val)
);
}, [getActorInfo, id, isMounted]);
you need to cleanup useEffect like
useEffect(() => {
getActorInfo(id).then(val => setActorInfo(val));
return () => {
setActorInfo({});
}
},[]);
have a look at this article. It explains you why to cleanup useEffect.
Related
I have this component that when I click on the button it toggles the state of isMenuActive, the problem comes when implementing the function inside the useEffect, for a strange reason when I try to access to the value of isMenuActive inside the closeTooltip function it always reflects false, even though the value of isMenuActive has changed to true.
import { useState, useEffect, useRef } from "react";
import Tooltip from "./Tooltip";
export default function Navbar() {
const [isMenuActive, setIsMenuActive] = useState(false);
const menu = useRef(null);
const handleOnClick = (e) => {
setIsMenuActive((prev) => !prev);
};
useEffect(() => {
const closeTooltip = (e) => {
if (menu.current && !menu.current.contains(e.target)) {
//Here isMenuActive is always false, even after using handleOnClick func
console.log(isMenuActive);
}
};
document.addEventListener("click", closeTooltip, true);
return () => document.removeEventListener("click", closeTooltip, true);
}, [menu]);
return (
<button
className="navbar__notifications"
aria-label="Notificaciones"
name="notification"
onClick={handleOnClick}
ref={menu}
>
{isMenuActive && <Tooltip title="Notificaciones" />}
</button>
);
}
Any idea of why is this happening?
menu (with useRef) always refers to the same object, so useEffect won't be triggered for every change of menu (even though menu.current).
useEffect is only triggered in 3 cases
Props change with dependencies
States change with dependencies
Initial mounted component
The log you get from closeTooltip is from the initial useEffect call (initial mounted component)
For the fix, you should add isMenuActive (state changes) in the dependency list of useEffect instead. That will keep listening to isMenuActive state changes and update your closeTooltip event accordingly.
useEffect(() => {
const closeTooltip = (e) => {
if (menu.current && !menu.current.contains(e.target)) {
//Here isMenuActive is always false, even after using handleOnClick func
console.log(isMenuActive);
}
};
document.addEventListener("click", closeTooltip, true);
return () => document.removeEventListener("click", closeTooltip, true);
}, [isMenuActive]);
You can check this sandbox for the test
Anyone how can tell me why the ref doesn't get updated on the initial render?
I am setting the ref conditionally on the last item, which work fine if I don't set the values async.
const [values, setValues] = useState<Array<Number>>([]);
const ref = useRef(null);
useEffect(() => {
console.log(ref.current);
}, [ref]);
useEffect(() => {
setTimeout(() => {
const newValues = Array(10)
.fill(0)
.map((item) => Math.random());
setValues(newValues);
}, 1000);
}, []);
return (
<div className="App">
{values.map((value, index) => {
const isLast = index === values.length - 1;
return (
<div key={value.toString()} {...(isLast && { ref: ref })}>
{value}
</div>
);
})}
</div>
);
Codesandbox
The reason why you don't see useEffect witth dependency as ref executed when you populatee the values async is because the ref instance remains same but the current key within tthe ref object is modified.
If you track on ref.current, you will see the useEffect get executed.
useEffect(() => {
console.log(ref.current);
}, [ref.current]);
However that is not the right approach because a change in ref is not triggering a re-render and useEffect is called post a re-render. What you should instead rely on as a dependency for your useEffect is a change in values which essentially causes the ref to change
useEffect(() => {
console.log(ref.current);
}, [values]);
DEMO sandbox
I have created a custom hook to scroll the element back into view when the component is scrolled.
export const useComponentIntoView = () => {
const ref = useRef();
const {current} = ref;
if (current) {
window.scrollTo(0, current.offsetTop );
}
return ref;
}
Now i am making use of this in a functional component like
<div ref={useComponentIntoView()}>
So for the first time the current always comes null, i understand that the component is still not mounted so the value is null . but what can we do to get this values always in my custom hook as only for the first navigation the component scroll doesn't work . Is there any work around to this problem .
We need to read the ref from useEffect, when it has already been assigned. To call it only on mount, we pass an empty array of dependencies:
const MyComponent = props => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return <div ref={ref} />;
};
In order to have this functionality out of the component, in its own Hook, we can do it this way:
const useComponentIntoView = () => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return ref;
};
const MyComponent = props => {
const ref = useComponentIntoView();
return <div ref={ref} />;
};
We could also run the useEffect hook after a certain change. In this case we would need to pass to its array of dependencies, a variable that belongs to a state. This variable can belong to the same Component or an ancestor one. For example:
const MyComponent = props => {
const [counter, setCounter] = useState(0);
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, [counter]);
return (
<div ref={ref}>
<button onClick={() => setCounter(counter => counter + 1)}>
Click me
</button>
</div>
);
};
In the above example each time the button is clicked it updates the counter state. This update triggers a new render and, as the counter value changed since the last time useEffect was called, it runs the useEffect callback.
As you mention, ref.current is null until after the component is mounted. This is where you can use useEffect - which will fire after the component is mounted, i.e.:
const useComponentIntoView = () => {
const ref = useRef();
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop );
}
});
return ref;
}
Do all variables used inside a useEffect always, absolutely, without exception need to be specified as dependencies?
My use case (simplified for demonstration purposes) involves different functions being executed depending on the width of the browser window, but not run when browser window changes:
const scrollToTop = () => window.scrollTo(0, 0)
const scrollToTopOfArticle = () => window.scrollTo(0, 200)
function App({
isDesktop,
selectedArticle
}) {
useEffect(() => {
isDesktop ? scrollToTop() : scrollToTopOfArticle()
}, [selectedArticle])
return (
<div className="App">
<h1>{selectedArticle.title}</h1>
<p>{selectedArticle.body}</p>
</div>
);
}
If I add isDesktop to the dependencies then the effect runs whenever the user resizes the window between mobile and desktop, which is not desired, but I'm also aware of the dogma that everything used inside the effect must be listed as a dependency.
Any suggestions on how to reconcile these two requirements?
If you want to make a useEffect() only responsive to a change in selectedArticle, use isDesktop and selectedArticle to initialize component states. Whenever selectedArticle changes, the first useEffect() will update both states with the passed-in props, and trigger the second useEffect() to re-run on the next render.
const scrollToTop = () => window.scrollTo(0, 0)
const scrollToTopOfArticle = () => window.scrollTo(0, 200)
function App({
isDesktop,
selectedArticle
}) {
const [desktop, setDesktop] = useState(isDesktop)
const [article, setArticle] = useState(selectedArticle)
useEffect(() => {
if (article !== selectedArticle) {
setDesktop(isDesktop)
setArticle(selectedArticle)
}
}, [isDesktop, selectedArticle, article])
useEffect(() => {
if (desktop) scrollToTop()
else scrollToTopOfArticle()
}, [desktop, article])
return (
<div className="App">
<h1>{selectedArticle.title}</h1>
<p>{selectedArticle.body}</p>
</div>
)
}
Alternatively, you can abstract this latching behavior to another hook so that isDesktop only updates to its live value when selectedArticle changes. Note that selectedArticle still needs to be a dependency of the scroll action effect, so that the useEffect() will trigger the scroll action on every change to selectedArticle even if isDesktop has not changed values since the last trigger.
const useLatch = (value, deps) => {
const [state, setState] = useState(value)
const effect = useCallback(() => { setState(value) }, [value])
useEffect(effect, deps)
return state
}
const scrollToTop = () => window.scrollTo(0, 0)
const scrollToTopOfArticle = () => window.scrollTo(0, 200)
function App({
isDesktop,
selectedArticle
}) {
const latchedIsDesktop = useLatch(isDesktop, [selectedArticle])
useEffect(() => {
if (latchedIsDesktop) scrollToTop()
else scrollToTopOfArticle()
}, [latchedIsDesktop, selectedArticle])
return (
<div className="App">
<h1>{selectedArticle.title}</h1>
<p>{selectedArticle.body}</p>
</div>
)
}
Are all variables inside a useEffect absolutely required to be listed as dependencies?
Yes or else it will generate a warning.
That's why it's best to have clear idea what each effect should do (i.e separation of concern).
Thus, you can have two separate effects with different dependencies.
Something like:
useEffect(
() => {
scrollToTopOfArticle();
}, [selectedArticle]
);
useEffect(
() => {
if (selectedArticle && isDesktop) {
scrollToTop();
}
}, [isDesktop, selectedArticle]
)
I am using ReactHooks. I am trying to access ref of User component in useEffect function, but I am getting elRef.current value as null, though I passed elRef.current as second argument to useEffect. I am supposed to get reference to an element, but outside (function body) of useEffect, ref value is available. Why is that ? How can I get elRef.current value inside useEffect?
code
import React, { Component, useState, useRef, useEffect } from "react";
const useFetch = url => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(
() => {
setIsLoading(true);
fetch(url)
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then(json => {
setIsLoading(false);
setData(json.data);
})
.catch(error => {
setIsLoading(false);
setError(error);
});
},
[url]
);
return { data, isLoading, error };
};
const User = ({ id }) => {
const elRef = useRef(null);
const { data: user } = useFetch(`https://reqres.in/api/users/${id}`);
useEffect(() => {
console.log("ref", elRef.current);
}, [elRef.current]);
if (!user) return null;
return <div ref={elRef}>{user.first_name + " " + user.last_name}</div>;
};
class App extends Component {
state = {
userId: 1
};
handleNextClick = () => {
this.setState(prevState => ({
userId: prevState.userId + 1
}));
};
handlePrevNext = () => {
this.setState(prevState => ({
userId: prevState.userId - 1
}));
};
render() {
return (
<div>
<button
onClick={() => this.handlePrevClick()}
disabled={this.state.userId === 1}
>
Prevoius
</button>
<button onClick={() => this.handleNextClick()}>Next</button>
<User id={this.state.userId} />
</div>
);
}
}
export default App;
Codesandbox link
Thanks !
You should use useCallback instead of useRef as suggested in the reactjs docs.
React will call that callback whenever the ref gets attached to a different node.
Replace this:
const elRef = useRef(null);
useEffect(() => {
console.log("ref", elRef.current);
}, [elRef.current]);
with this:
const elRef = useCallback(node => {
if (node !== null) {
console.log("ref", node); // node = elRef.current
}
}, []);
It's a predictable behaviour.
As mentioned #estus you faced with this because first time when it's called on componentDidMount you're getting null (initial value) and get's updated only once on next elRef changing because, actually, reference still being the same.
If you need to reflect on every user change, you should pass [user] as second argument to function to make sure useEffect fired when user is changed.
Here is updated sandbox.
Hope it helped.
When you use a function as a ref, it is called with the instance when it is ready. So the easiest way to make the ref observable is to use useState instead of useRef:
const [element, setElement] = useState<Element | null>(null);
return <div ref={setElement}></div>;
Then you can use it in dependency arrays for other hooks, just like any other const value:
useEffect(() => {
if (element) console.log(element);
}, [element]);
See also How to rerender when refs change.
useEffect is used as both componentDidMount and componentDidUpdate,
at the time of component mount you added a condition:
if (!user) return null;
return <div ref={elRef}>{user.first_name + " " + user.last_name}</div>;
because of the above condition at the time of mount, you don't have the user, so it returns null and div is not mounted in the DOM in which you are adding ref, so inside useEffect you are not getting elRef's current value as it is not rendered.
And on the click of next as the div is mounted in the dom you got the value of elRef.current.
The assumption here is that useEffect needs to detect changes to ref.current, so needs to have the ref or ref.currentin the dependencies list. I think this is due to es-lint being a bit over-pedantic.
Actually, the whole point of useEffect is that it guarantees not to run until the rendering is complete and the DOM is ready to go. That is how it handles side-effects.
So by the time useEffect is executed, we can be sure that elRef.current is set.
The problem with your code is that you don't run the renderer with <div ref={elRef}...> until after user is populated. So the DOM node you want elRef to reference doesn't yet exist. That is why you get the null logging - nothing to do with dependencies.
BTW: one possible alternative is to populate the div inside the effect hook:
useEffect(
() => {
if(!user) return;
elRef.current.innerHTML = `${user.first_name} ${user.last_name}`;
}, [user]
);
...
//if (!user) return null;// Remove this line
return <div ref={elRef}></div>; //return div every time.
That way the if (!user) return null; line in the User component is unnecessary. Remove it, and elRef.current is guaranteed to be populated with the div node from the very beginning.
set a useEffect on the elem's.current:
let elem = useRef();
useEffect(() => {
// ...
}, [elem.current]);