I am having trouble preventing unnecessary side-effects from a useEffect hook linked to a component that gets re-rendered/reused multiple times after being clicked. I want the side-effect to trigger an action once, but it is being triggered for every render.
I'm building a task viewer that displays nested task data. Please see screenshot #1 for reference.
For context, the main display shows the main tasks (see Event Data) and renders a clickable button if the task has sub-events. When this button is clicked, the selected main task is displayed at the top of the hierarchy view (see Event Hierarchy) and its sub-events are displayed below the hierarchy in a separate pane (see Event Details).
Like the main tasks, if these sub-events in 'Event Details' have their own sub-events, they are also rendered with a clickable button. When this sub-event button is clicked, this clicked sub-event is added to the bottom of the hierarchy, where the clicked main task is already displayed in bold. This selected sub-event's sub-events then replace the content in the 'Event Details' pane.
As the user clicks through the nested data, the clicked sub-event is added to the bottom of the hierarchy so that the user has an idea of where he is in the nested data and its sub-events displayed in 'Event Details'. All 'Event Hierarchy' and 'Event Details' data is cleared when the user selects a new main event or selects a new page.
The hierarchy events are held in an array managed via useState and every time another sub-event is clicked, it is added to this array. That's the idea, at least.
#1
My problem is this:
If I place my setHierarchy function inside a useEffect hook with the selectedTask as dependency, it renders the selectedTask in the hierarchy instantaneously, but the button component that triggers setHierarchy is re-rendered for every sub-event being displayed in 'Event Details' (as I want each event to be clickable) and in doing so, it adds that many copies of the event to my hierarchy array. This happens even though I am checking to see if the hierarchy array already contains the selected subevent before adding it. See result in screenshot #2.
I have tried various configurations of checking the array, but I cannot seem to stop it from adding these copies to and subsequently displaying them in the Hierarchy.
If I place the setHierarchy function inside my click handler, only one single event is added, but it executes before the selectedSubEvent has been updated. This means the hierarchy array is empty upon first render and stays one click 'behind' ie. a specific event is only displayed upon the following click event, after the click that selected it.
#2
This is all done inside my ExpandSubEvents button component (see code below) and also managed via a context provider.
I have tried moving the setHierarchy into a separate function, inside a useCallback, and triggering it from both the clickHandler and the useEffect that sets the selectedSubEvent. This did not resolve the issue.
I've also tried useRef to try and link it to the latest state. I'm not sure that's even doable/correct.
What am I doing wrong here? I am fairly new to coding, so any input on this would be much appreciated.
Sidenote: I suspect that my setup is perhaps beyond the intended scope of useContext. Is it? What can I do to make improvements? Is this perhaps in any way responsible for my issue?
Thank you for taking your time to read this far. I appreciate it!
Deon
ExpandSubEvents Component
import React, { useCallback, useContext, useEffect, useMemo } from 'react';
import SubEventContext from '../../store/sub-event-context';
import classes from './ExpandSubEvents.module.css';
const ExpandSubEvents: React.FC<{
id: number;
subEvents: number;
}> = React.memo((props) => {
// Extract context
const subEventCtx = useContext(SubEventContext);
const {
subEvents,
subEventParentId,
selectedSubEvent,
hierarchy,
setSubEventParentId,
setFetchId,
setSelectedSubEvent,
setHierarchy,
} = subEventCtx;
// Get id of event for when it is clicked
const id = React.useMemo(() => props.id, [props.id]);
let eventIds: number[] = useMemo(() => [], []);
if (hierarchy) {
for (const event of hierarchy) {
eventIds.push(event.id);
}
}
// Set CSS classes to style button if it has sub-events
let subEventQuantity = props.subEvents;
let importedClasses = `${classes['sub-event-button']}`;
if (subEventQuantity === 0) {
importedClasses = `${classes['no-sub-events']}`;
}
// Push the event to the Hierarchy display
// NOTE Tried moving the setHierarchy to a separate function, but it did not make a difference
// const triggerHierarchy = useCallback(() => {
// if (!eventIds.includes(id))
// setHierarchy((prevState) => [...prevState, ...selectedSubEvent]);
// }, [eventIds, id, selectedSubEvent, setHierarchy]);
// Respond to subevent button click event
const clickHandler = useCallback(() => {
setSubEventParentId(id);
setFetchId(id);
// This setHierarchy works, but executes before the selectedSubEVent has been updated
// Furthermore, if a new subevent is selected, it checks if the NEW clicked one has been added
// BUT sends the OLD event still in selectedSubEvent to the hierarchy before IT has been updated
// meaning that the check does not stop the same event being added twice
if (!eventIds.includes(id))
setHierarchy((prevState) => [...prevState, ...selectedSubEvent]);
}, [
eventIds,
id,
selectedSubEvent,
setFetchId,
setHierarchy,
setSubEventParentId,
]);
// NOTE Tried useRef to get setHierarchy to use the latest selectedSubEvent
// const subEventRef = useRef<Event[]>([]);
// subEventRef.current = hierarchy;
// Trying to setHierarchy directly from its own useEffect
// useEffect(() => {
// if (!eventIds.includes(id))
// setHierarchy((prevState) => [...prevState, ...selectedSubEvent]);
// }, [eventIds, hierarchy, id, selectedSubEvent, setHierarchy]);
// Filter the event from the subEvent array and set it to selectedSubEvent
useEffect(() => {
setSelectedSubEvent(
subEvents.filter((subEvent) => subEvent.id === subEventParentId)
);
}, [setSelectedSubEvent, subEventParentId, subEvents]);
return (
<button onClick={clickHandler} className={importedClasses}>
{subEventQuantity}
</button>
);
});
export default ExpandSubEvents;
I use react js for my application.
I have a function which is called when I click on the button.
const click = () => {
dispatch(postData)
console.log(selector.createUser.error.message;)
}
When I click on button submit i want to get the error message in console.log(selector.createUser.error.message;) , but i get undefined, and if i click second time i get the message.
Why it is happening and ho to solve? How to get the exact value in click function clicking first time?
const click = () => {
dispatch(postData);
console.log({selector.createUser.error.message});
}
Something like that should work. Depending on what selector is.
Note: this snippet doesn't actually work since nothing is defined but this shows how you would log a variable.
The background page of modal window in Unsplash is really unique.
Emulation:
click on photo from feed
then click to another photo on modal page to watch effect
refresh page
click to go back on browser
notice the route of background location on modal window
So my question is how to create this background location properly?
Ciao, my personal point of view is that when you go back, Unsplash page re-triggers the modal window you open before. Unspalsh is written in React so, for example, code could be like that:
function Page() {
const [state, setState] = useState({...});
useEffect(() => {
// get data from cache
if (data) {
// (re)open dialog with photo
}
}, [state]); // state in deps so useEffect will be re-triggered each time you enter in Page
}
I don't want to be reductive but this could be a start point.
I'm using Material-UI component.
This one from git : https://github.com/mui-org/material-ui/blob/master/docs/src/pages/components/tabs/ScrollableTabsButtonAuto.js
My main problem is when i use the scroll button, that scroll too much ( i have lot of tabs, something like 20 tabs ). So when i click on scroll button, that scroll to the right a lot and i lose my active tab.
I have tried to check the code of Tabs.
I found the 2 function i have to override.
These functions are inside that js code:
https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Tabs/Tabs.js
var handleStartScrollClick = function handleStartScrollClick() {
moveTabsScroll(-tabsRef.current[clientSize]);
};
var handleEndScrollClick = function handleEndScrollClick() {
moveTabsScroll(tabsRef.current[clientSize]);
};
If i modify it on node_modules directy that work well, here an exemple :
var handleStartScrollClick = function handleStartScrollClick() {
moveTabsScroll(-tabsRef.current[clientSize]/10);
};
var handleEndScrollClick = function handleEndScrollClick() {
moveTabsScroll(tabsRef.current[clientSize]/10);
};
So now when i click on scroll button i have good result, it's scrolling step by step. But i can't directly overide into node_modules.
How can i do to do it correctly & properly please ?
i have a list where i can access detail views of my list items.
in my detail views i can navigate betweeen my list items.
i would like to have a back button on the detail view which brings me to my list view no matter how often i navigated in my detail views.
currently i tried this:
i pass a url: string to my navigation component.
thats the url to my list.
then i want to change my state when i click the back button:
const onClickBack = () => {
const stateData = {
path: window.location.href,
scrollTop: 300
}
window.history.replaceState(stateData, "", backURL)
const stateData2 = {
path: window.location.href,
scrollTop: 300
}
window.history.pushState(stateData2, "", backURL)
window.history.back()
}
i copied this code from history pushState and scroll position
no matter how often i navigated, i can go back to my list now. but how can i restore the whole state, e.g. the scroll position?
do i have to pass the whole state object?
It seems like you want to manually restore the scroll position. If this is the case, then you should tell the history API about your intention.
if ('scrollRestoration' in history) {
// Back off, browser, I got this...
history.scrollRestoration = 'manual';
}
This way, the scroll position of the previous URL will remain as well.
docs: https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration