React state variable not update in callback function from IntersectionObserver - reactjs

I'm trying to make an infinite scroll function with IntersectionObserver. When the callback is called I make a test to see if there is some loading happens and if so I want to cancel that new call. The function is that:
const loadMore = useCallback(() => {
if (loading) {
return
}
setLoading(true);
console.log('Loading');
setTimeout(() => {
console.log('Finished');
setLoading(false);
}, 5000);
}, []);
The problem is that when the watcher calls loadMore again, and there is a load going on at that moment, the loading state sometimes has the old value (false), and setTimeOut is called again even if there is another one running. If I put the loading in the dependency array, the function is called many times (in loop) because I change it's value inside it. I tried to use a useRef to control loading, it works in the function, but I also want to use the loading state in JSX to show or not a text, but with the useRef variable it doesn't work in JSX and I find it a little ugly having to use two variables to control almost the same thing. Is there a way to do this just with the state?
The observer code:
useEffect(() => {
const options = {
root: null,
rootMargin: '10px',
threshold: 1.0
}
const observer = new IntersectionObserver(loadMore, options);
if (loaderRef && loaderRef.current) {
observer.observe(loaderRef.current);
}
}, [loadMore]);

Related

Unintentional deleting reference to an object on useEffect in ReactJs

I am new to ReactJs, but I need to maintain some preexisting codebase. There is one behavior that I can't understand (and get around it).
I have a Store object that holds data that needs to be rendered with the Cytoscape library.
Currently, I have useEffect trigger on the Store object that creates data for Cy lib and populates the library. Now I would like to skip some updates when they are not necessary. Something like this (it is pseudocode):
useEffect(() => {
if(store.dontNeedRerender) {
console.log("Skip this rendering");
} else {
const elements = convertDataToCyData();
const cy: cytoscape.Core = cytoscape({
container: document.getElementById("cytoscape-dataset-panel"),
// #ts-ignore
elements,
// #ts-ignore
style: style,
...store.config,
});
graphInit(cy);
setCy(cy);
}
}, [store]);
My problem is, that every time the useEffect() is called cy is emptied (library rendered empty screen), and then repopulate. But if store.dontNeedRerender is true, the screen remains empty (instead of just skipping rerendering...)
I guess this is something to do with state management... but I can't figure this out.
// useEffect with empty dependency
useEffect(() => {
// some function
console.log("I am rendered");
// this only trigger once on first render
}, []);
// useEffect with 'one' dependency
useEffect(() => {
// some function
console.log("I am rendered");
// this only trigger twice on first render
// then render everytime variable 'one' change
}, [one]);
// useEffect with dependency which is a boolean
useEffect(() => {
// this will rerender when condition change
obj.someCondition ? console.log(obj.name) : undefined;
// adding the whole object 'obj' as dependency is not necessary
// if obj.name will be the same in every re-render,
// don't add it to dependency
}, [obj.someCondition]);

how to use the useEffect hook on component unmount to conditionally run code

For some odd reason the value of props in my "unmount" useEffect hook is always at the original state (true), I can console and see in the devtools that it has changed to false but when the useEffect is called on unmount it is always true.
I have tried adding the props to the dependancies but then it is no longer called only on unmount and does not serve it's purpose.
Edit: I am aware the dependancy array is empty, I cannot have it triggered on each change, it needs to be triggered ONLY on unmount with the update values from the props. Is this possible?
React.useEffect(() => {
return () => {
if (report.data.draft) { // this is ALWAYS true
report.snapshot.ref.delete();
}
};
}, []);
How can I conditionally run my code on unmount with the condition being dependant on the updated props state?
If you want code to run on unmount only, you need to use the empty dependency array. If you also require data from the closure that may change in between when the component first rendered and when it last rendered, you'll need to use a ref to make that data available when the unmount happens. For example:
const onUnmount = React.useRef();
onUnmount.current = () => {
if (report.data.draft) {
report.snapshot.ref.delete();
}
}
React.useEffect(() => {
return () => onUnmount.current();
}, []);
If you do this often, you may want to extract it into a custom hook:
export const useUnmount = (fn): => {
const fnRef = useRef(fn);
fnRef.current = fn;
useEffect(() => () => fnRef.current(), []);
};
// used like:
useUnmount(() => {
if (report.data.draft) {
report.snapshot.ref.delete();
}
});
The dependency list of your effect is empty which means that react will only create the closure over your outer variables once on mount and the function will only see the values as they have been on mount. To re-create the closure when report.data.draft changes you have to add it to the dependency list:
React.useEffect(() => {
return () => {
if (report.data.draft) { // this is ALWAYS true
report.snapshot.ref.delete();
}
};
}, [report.data.draft]);
There also is an eslint plugin that warns you about missing dependencies: https://www.npmjs.com/package/eslint-plugin-react-hooks
Using custom js events you can emulate unmounting a componentWillUnmount even when having dependency. Here is how I did it.
Problem:
useEffect(() => {
//Dependent Code
return () => {
// Desired to perform action on unmount only 'componentWillUnmount'
// But it does not
if(somethingChanged){
// Perform an Action only if something changed
}
}
},[somethingChanged]);
Solution:
// Rewrite this code to arrange emulate this behaviour
// Decoupling using events
useEffect( () => {
return () => {
// Executed only when component unmounts,
let e = new Event("componentUnmount");
document.dispatchEvent(e);
}
}, []);
useEffect( () => {
function doOnUnmount(){
if(somethingChanged){
// Perform an Action only if something changed
}
}
document.addEventListener("componentUnmount",doOnUnmount);
return () => {
// This is done whenever value of somethingChanged changes
document.removeEventListener("componentUnmount",doOnUnmount);
}
}, [somethingChanged])
Caveats: useEffects have to be in order, useEffect with no dependency have to be written before, this is to avoid the event being called after its removed.

Infinite re-render in functional react component

I am trying to set the state of a variable "workspace", but when I console log the data I get an infinite loop. I am calling the axios "get" function inside of useEffect(), and console logging outside of this loop, so I don't know what is triggering all the re-renders. I have not found an answer to my specific problem in this question. Here's my code:
function WorkspaceDynamic({ match }) {
const [proposals, setProposals] = useState([{}]);
useEffect(() => {
getItems();
});
const getItems = async () => {
const proposalsList = await axios.get(
"http://localhost:5000/api/proposals"
);
setProposals(proposalsList.data);
};
const [workspace, setWorkspace] = useState({});
function findWorkspace() {
proposals.map((workspace) => {
if (workspace._id === match.params.id) {
setWorkspace(workspace);
}
});
}
Does anyone see what might be causing the re-render? Thanks!
The effect hook runs every render cycle, and one without a dependency array will execute its callback every render cycle. If the effect callback updates state, i.e. proposals, then another render cycle is enqueued, thus creating render looping.
If you want to only run effect once when the component mounts then use an empty dependency array.
useEffect(() => {
getItems();
}, []);
If you want it to only run at certain time, like if the match param updates, then include a dependency in the array.
useEffect(() => {
getItems();
}, [match]);
Your use of useEffect is not correct. If you do not include a dependency array, it gets called every time the component renders. As a result your useEffect is called which causes setProposals then it again causes useEffect to run and so on
try this
useEffect(() => {
getItems();
} , []); // an empty array means it will be called once only
I think it's the following: useEffect should have a second param [] to make sure it's executed only once. that is:
useEffect(() => {
getItems();
}, []);
otherwise setProposal will modify the state which will trigger a re-render, which will call useEffect, which will make the async call, which will setProposal, ...

Infinite Loop with useEffect - ReactJS

I have a problem when using the useEffect hook, it is generating an infinite loop.
I have a list that is loaded as soon as the page is assembled and should also be updated when a new record is found in "developers" state.
See the code:
const [developers, setDevelopers] = useState<DevelopersData[]>([]);
const getDevelopers = async () => {
await api.get('/developers').then(response => {
setDevelopers(response.data);
});
};
// This way, the loop does not happen
useEffect(() => {
getDevelopers();
}, []);
// This way, infinte loop
useEffect(() => {
getDevelopers();
}, [developers]);
console.log(developers)
If I remove the developer dependency on the second parameter of useEffect, the loop does not happen, however, the list is not updated when a new record is found. If I insert "developers" in the second parameter of useEffect, the list is updated automatically, however, it goes into an infinite loop.
What am I doing wrong?
complete code (with component): https://gist.github.com/fredarend/c571d2b2fd88c734997a757bac6ab766
Print:
The dependencies for useEffect use reference equality, not deep equality. (If you need deep equality comparison for some reason, take a look at use-deep-compare-effect.)
The API call always returns a new array object, so its reference/identity is not the same as it was earlier, triggering useEffect to fire the effect again, etc.
Given that nothing else ever calls setDevelopers, i.e. there's no way for developers to change unless it was from the API call triggered by the effect, there's really no actual need to have developers as a dependency to useEffect; you can just have an empty array as deps: useEffect(() => ..., []). The effect will only be called exactly once.
EDIT: Following the comment clarification,
I register a developer in the form on the left [...] I would like the list to be updated as soon as a new dev is registered.
This is one way to do things:
The idea here is that developers is only ever automatically loaded on component mount. When the user adds a new developer via the AddDeveloperForm, we opportunistically update the local developers state while we're posting the new developer to the backend. Whether or not posting fails, we reload the list from the backend to ensure we have the freshest real state.
const DevList: React.FC = () => {
const [developers, setDevelopers] = useState<DevelopersData[]>([]);
const getDevelopers = useCallback(async () => {
await api.get("/developers").then((response) => {
setDevelopers(response.data);
});
}, [setDevelopers]);
useEffect(() => {
getDevelopers();
}, [getDevelopers]);
const onAddDeveloper = useCallback(
async (newDeveloper) => {
const newDevelopers = developers.concat([newDeveloper]);
setDevelopers(newDevelopers);
try {
await postNewDeveloperToAPI(newDeveloper); // TODO: Implement me
} catch (e) {
alert("Oops, failed posting developer information...");
}
getDevelopers();
},
[developers],
);
return (
<>
<AddDeveloperForm onAddDeveloper={onAddDeveloper} />
<DeveloperList developers={developers} />
</>
);
};
The problem is that your getDevelopers function, calls your setDevelopers function, which updates your developers variable. When your developers variable is updated, it triggers the useEffect function
useEffect(() => {
getDevelopers();
}, [developers]);
because developers is one of the dependencies passed to it and the process starts over.
Every time a variable within the array, which is passed as the second argument to useEffect, gets updated, the useEffect function gets triggered
Use an empty array [] in the second parameter of the useEffect.
This causes the code inside to run only on mount of the parent component.
useEffect(() => {
getDevelopers();
}, []);

React Hooks: Idiomatic way to ensure that useEffect runs only when the contents of an array argument to the custom hook change

I am creating a custom hook in React which sets up an event listener for a given set of events. A default set of events exists, and the consumer of this custom hook is not expected to customize these in the majority of use-cases. Generally, I want the event listeners to be added upon the mounting of a component and removed upon its un-mounting. However, adhering to the principles of hooks (and the eslint(react-hooks/exhaustive-deps) lint rule), I wish to gracefully handle changes to the list of events to watch. What is the most idiomatic way to achieve this with React hooks?
Assuming I would simply like to remove all event listeners and re-add them when the list of events changes, I could attempt the following:
const useEventWatcher = (
interval = 5000,
events = ['mousemove', 'keydown', 'wheel']
) => {
const timerIdRef = useRef();
useEffect(() => {
const resetInterval = () => {
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
}
timerIdRef.current = setInterval(() => {
console.log(`${interval} milliseconds passed with no ${events.join(', ')} events!`);
}, interval)
};
events.forEach(event => window.addEventListener(event, resetInterval));
// Don't want to miss the first time the interval passes without
// the given events triggering (cannot run the effect after every render due to this!)
resetInterval();
return () => {
events.forEach(event => window.removeEventListener(event, resetInterval));
};
}, [events, interval]);
}
Unfortunately, this will not function as intended. Note that I would like to provide a default value for the events parameter. Doing that with the current approach means that events points to a different reference every time the custom hook runs, which means the effect runs every time as well (due to shallow dependency comparison). Ideally, I would like a way of having the effect depend on the contents of the array, rather than the reference. What is the best way of achieving this?
You can separate two side effects in two different useEffects.
You can run the initial resetInterval in the first useEffect on load.
You need to run it once, and you might use a dependency of [].
But then, you need to extract resetInterval outside the useEffect.
Another problem is that, now resetInterval is re-created during every render.
So you can wrap it in useCallback.
The first useEffect depends on resetInterval (which causes the useEffect run once on load, thus will call resetInterval)
Now you can subscribe all events in the second useEffect with dependencies on [events, interval, resetInterval] as suggested by "eslint(react-hooks/exhaustive-deps) lint rule".
The result would look like following &
You can check the working demo on CodeSandbox.
const useEventWatcher = (
interval = 2000,
events = ["mousemove", "keydown", "wheel"]
) => {
const timerIdRef = useRef();
const resetInterval = useCallback(() => {
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
}
timerIdRef.current = setInterval(() => {
console.log(
`${interval} seconds passed with no ${events.join(", ")} events!`
);
}, interval);
}, [events, interval]);
useEffect(() => {
resetInterval();
}, [resetInterval]);
useEffect(() => {
events.forEach(event => window.addEventListener(event, resetInterval));
return () => {
events.forEach(event => window.removeEventListener(event, resetInterval));
};
}, [events, interval, resetInterval]);
};
Check out the working page (I set the interval to 2 seconds for demo purpose)
When you move the mouse around, scroll wheels or press keys, the console log won't appear. Once those events are not fired, then you will see the console log messages.
So we need to include events into deps but for sure we don't want endless render loop.
Option #1: use JSON.stringify or similar to pass string as dependency not an array
function useEventWatcher(events = ['click'])
useEffect(() => {
}, [JSON.stringifiy(events.sort())])
However ESLint will still complain so either suppress it or use de-stringify:
const eventsStringified = JSON.stringify(events.sort());
useEffect(() => {
const eventsUnstringified = JSON.parse(eventsStringified);
}, [eventStringified]);
Option #2: move setting default value into useMemo. So default values will be referentially the same while events parameter is not passed(so it is undefined)
function useEventWatcher(events) {
const eventsOrDefault = useMemo(() => eventsPassed || ['click'], [events]);
useEffect(() => {
}, [eventsOrDefault]);
}

Resources