React hooks - Prevent rerender if parent state that holds children props changed - reactjs

I have an issue with my current project that uses react hooks.
What I'm trying to do is just to select my tasks by using (shift+click). Look like this:
Here is the code:
...
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([])
const selectTask = useCallback(
(e: MouseEvent<HTMLDivElement>, taskId: string): void => {
e.stopPropagation()
const previousTaskId = selectedTaskIds[selectedTaskIds.length - 1]
if (previousTaskId && e.shiftKey) {
// handle shift+click
const previousIdx = tasks.findIndex((task) => task.id === previousTaskId)
const selectedIdx = tasks.findIndex((task) => task.id === taskId)
const rangeTasks =
previousIdx < selectedIdx
? tasks.slice(previousIdx, selectedIdx + 1)
: tasks.slice(selectedIdx, previousIdx + 1)
const rangeIds = rangeTasks.map((task) => task.id)
setSelectedTaskIds([...new Set([...selectedTaskIds, ...rangeIds])])
} else {
// if no key clicked, just select 1 task item
setSelectedTaskIds([taskId])
}
},
[selectedTaskIds, tasks] // <==== in here I notice that activeTaskIds is changed overtime that causes all of my <TaskItem> rerender
)
return (
{tasks.map((task) => (
<TaskItem
key={task.id}
taskId={task.id}
onClick={selectTask} // <=== selectTask will be different if user click on one of the task items
active={selectedTaskIds.includes(task.id)}
/>
))}
)
The problem is, to know which tasks should I select when the user uses shift+click, I need to know the currently selected task ids, so that I need to pass selectedTaskIds as a useCallback() deps.
That makes whenever the user selects the tasks or even just a click on one of the task items to select the task, it will re-render all of my <TaskItem> since the selectTask() function change due to useCallback's deps changed.
How can I solve this without rerender all of my <TaskItem>s? Thank you so much!

I tested your code on my machine and tested out a few scenarios. As far as I can tell, it looks natural for the component to re-render the all of the <TaskItem>s because any change in the selectedTaskIds state will guarantee everything inside the component that holds selectedTaskIds to render. To show you a concrete example,
<div className="App">
<TaskItems />
<div>hahaha</div>
<div>selectedTaskIds</div>
</div>
Let's say you have the above code. (I named your component that holds multiple <TaskItem/>s as <TaskItems/>) When onClick of <TaskItem/> triggers, only <TaskItems/> will re-render. The two other divs are not re-rendered. However, if you place the two divs inside the <TaskItems/> component, they will re-render:
// assuming this is inside <TaskItems/>
...
return (
<div>
{tasks.map((task) => (
<TaskItem
key={task.id}
taskId={task.id}
onClick={(e) => { selectTask2(e, task.id)}} // <=== selectTask will be different if user click on one of the task items
// active={selectedTaskIds.includes(task.id)}
active={true}
title={task.title}
/>
))}
<div>hahaha</div>
<div>selectedTaskIds</div>
</div>
);
above code will re-render the two divs.
I have tried to fulfill your request to get rid of the re-renders of the tasks that weren't changed, but it was really hard to do so. When I try to prevent re-rendering I usually use one of the two techniques:
create a child component and separate the code base to isolate groups of states. (since states are what triggers renders, you can
separate unrelated ones into different groups.)
useCallback/useMemo
Either techniques I failed to implement for your case, but there may be a way to apply the above techniques. I will follow the thread to see if anyone else gets a solution.

Related

Conditional rendering with useEffect / useState, why is there a delay in the DOM when switching components?

Intro / Context
I am trying to build an application using React that allows for image or video display based on a chosen menu item. I am currently using Advanced Custom Fields within WordPress to build my data objects and using graphQL to query them into my project.
I want the application to display either a video component or an image component. This choice will be determined through conditional rendering and based on the contents of the object's fields.
Each object contains a title, an image field and a video field. If the entry in question should be displayed as an image the video field will be set as the string 'null'. All fields will return strings regardless.
I am using useState to target a particularly active field, however, despite triggering a change in state conditional rendering does not appear to change.
The Application
This is my approach
function Display({ objects }) {
const [setVisualOption, changeVisualOption] = useState(false);
const [appState, setState] = useState({
myObjects: objects,
activeTitle: "null",
activeImage: "null",
activeMediaUrl: "null",
});
function toggleActive(index, trackIndex) {
setState({
...appState,
activeTitle: appState.myObjects[index].title,
activeImage: appState.myObjects[index].image[0].mediaItemUrl,
activeMediaUrl: appState.myObjects[index].mediastreamurl,
});
changeVisualOption(appState.activeImage.includes("null"));
}
useEffect(() => {}, [
appState.activeTitle,
appState.activeImage,
appState.activeMediaUrl,
setVisualOption,
]);
return (
<div className="display">
<div className="list-box-right>
{appState.myObjects.map((element, index) => (
<>
<div
key={index}
className="menu-item"
onClick={() => {
toggleActive(index);
}}
>
{element.title}
</div>
</>
))}
</div>
<div className="right-grid">
{setVisualOption ? (
<VideoComponent activeImage={appState.activeImage}></VideoComponent>
) : (
<ImageComponent activeImage={appState.activeImage}></SingleImage>
)}
</div>
</div>
);
}
The summarise, to component takes objects as prop which are being passed down from another component making the graphQL query. I am then setting the initial values of useState as an object and setting an activeTitle, activeImage and activeMediaUrl as null.
I am then using a function to toggle the active items using the setState modifier based upon the index that is clicked within the return statement. From there I am using setVisualOption and evaluating whether the activeImage is contains 'null' (null.jpg), if this is true setVisualOption will be set to true allowing the Video Component to be rendered
The Problem
To be clear, there are no errors being produced and the problem is a slight rendering issue where it requires double clicks to alter the state and trigger the correct response from the tertiary operator.
The issue is within the conditional rendering. If I set my object fields to all images and return only the Image Component there are no issues, and the state change can be seen to register visually as you click down the listed options.
It is only when I introduce the conditional rendering that there is a delay, the first click does not generate the correct response from the component I am trying to display however the second click triggers the right response.
As you can see, I am also using useEffect to try and trigger a rendered response when any of the described states change but still encounter the same problem.
Does anyone know what is the cause of this bug? when looking at the console.log of setVisualOption is not appearing as true on first click when it aught to.
Any insights would be great thanks
You set your visual option right after you set your appState, this is why appState.activeImage in changeVisualOption is not updated because state updates in React is asynchronous. You can either use useEffect to update visual option when the appState changes or you can use appState.myObjects[index].image[0].mediaItemUrl in changeVisualOption
function toggleActive(index, trackIndex) {
setState({
...appState,
activeTitle: appState.myObjects[index].title,
activeImage: appState.myObjects[index].image[0].mediaItemUrl,
activeMediaUrl: appState.myObjects[index].mediastreamurl,
})
changeVisualOption(appState.myObjects[index].image[0].mediaItemUrl.includes("null"))
}
or
useEffect(() => {
changeVisualOption(appState.activeImage.includes("null"))
}, [appState])

React context not being up to date in a Timeout

I am using react app context to store an array of "alerts objects" which is basically any errors that might occur and I would want to show in the top right corner of the website. The issue I am having is that the context is not being up to date inside a timeout. What I have done for testing is gotten a button to add an alert object to the context when clicked and another component maps through that array in the context and renders them. I want them to disappear after 5 seconds so I have added a timeout which filters the item that got just added and removes it. The issue is that inside the timeout the context.alerts array seems to have the same value as 5 seconds ago instead of using the latest value leading to issues and elements not being filtered out. I am not sure if there's something wrong with my logic here or am I using the context for the wrong thing?
onClick={() => {
const errorPopup = getPopup(); // Get's the alert object I need
context.setAlerts([errorPopup, ...context.alerts]);
setTimeout(() => {
context.setAlerts([
...context.alerts.filter(
(element) => element.id !== errorPopup.id,
),
]);
}, 5000);
}}
onClick={() => {
const errorPopup = getPopup(); // Get's the alert object I need
context.setAlerts([errorPopup, ...context.alerts]);
setTimeout(() => {
context.setAlerts(alerts => [
...alerts.filter(
(element) => element.id !== errorPopup.id,
),
]);
}, 5000);
}}
This should fix it. Until react#17 the setStates in an event handler are batched ( in react#18 all setStates are batched even the async ones ), hence you need to use the most fresh state to make the update in second setAlerts.
To be safe it's a good practice using the cb syntax in the first setState as well.
I think the fix would be to move context.setAlerts(...) to a separate function (say removePopupFromContext(id:string)) and then call this function inside the setTimeout by passing the errorPopup.Id as parameter.
I'm not sure of your implementation of context.setAlerts, but if it's based on just setState function, then alternatively, you could do also something similar to how React let's you access prevState in setState using a function which will let you skip the creation of the extra function which may lightly translate to:
setContext(prevContextState =>({
...prevContextState,
alerts: prevContextState.alerts.filter(your condition)
)})

How to get a reference to a DOM element that depends on an input parameter?

Say I have the standard TODO app, and want a ref to the last item in the list. I might have something like:
const TODO = ({items}) => {
const lastItemRef = Reeact.useRef()
return {
<>
{items.map(item => <Item ref={item == items.last() ? lastItemRef : undefined} />)}
<>
}
}
But this doesn't seem to work - after lastItemRef is initialized, it is never subsequently updated as items are added to items. Is there a clean way of doing this without using a selector?
I think in your case it depends upon how the items list is updated. This is because useRef won't re-render the component if you change its current attribute (persistent). But it does re-render when you choose, for example, useState.
Just as a working case, see if this is what you were looking for.
Ps: check the console

React.js re-render behavior changes when list contents change

This question requires some background to understand:
My app uses various lists of items with Boolean state that the user toggles by clicking.
I implemented this logic with two reusable elements:
A custom hook, useSelections(), which maintains an array of objects of the form { id, name, isSelected }, where isSelected is Boolean and the only mutable element. It returns the array as current state, and a dispatch function that takes an input of the form { id, newValue } and updates the isSelected member of the object with the given id
A function component Selector, which takes as props a single item and the dispatch from useSelections and returns a <li> element whose CSS class depends on isSelected.
Normally, they are used together in the following way, which works fine (i.e., internal state and the color of the list item are synchronized, and toggle when clicked):
function localComponent(props) {
const [items, dispatch] = useSelections(props.data);
return (
<ul>
{items.map(item => <Selector key={item.id} item={item} onChange={dispatch} />)}
</ul>
);
}
It works equally well when useSelections() is elevated to a parent component, and items and dispatch are passed as props.
The trouble started when the array became larger, and I decided to page it:
<ul>
{items.slice(start, end).map(item => <Selector key={item.id} item={item} onChange={dispatch} />)}
</ul>
(start and end are part of component state.)
When my component is first rendered, it works normally. However, once start and end are changed (to move to the next page), clicking an item changes internal state, but no longer triggers a re-render. In UX terms this means that after the first 'next' click, when I click on an item nothing appears to happen, but if I hit 'next' again, and then 'back', the item I just clicked on has now changed.
Any suggestions would be appreciated.
I solved the problem by removing the logic from the reducer function to ignore calls where there is no change:
const hasChanged = modifiedItem.isSelected !== newValue;
modifiedItem.isSelected = newValue;
return hasChanged ? { data } : state;
is now simply:
modifiedItem.isSelected = newValue;
return { data };
There's still something going on with React here that I do not understand, but my immediate problem is solved.

How to change z index of components in React?

I forked the Keeper app project from Angela Yu's course on Udemy and made some modifications. Here is the link: https://codesandbox.io/s/keeper-app-part-2-completed-forked-9zks1?file=/src/components/Note.js
I wanted to be able to move the notes around the canvas, and while I was able to do that, I'm having a problem with the stacking of the notes. I want the selected note to be on top of all the other notes. I've tried tinkering with the z-index value by creating a useRef called noteRef and typing:
noteRef.current.style.zIndex = 9999
inside the handleOnClick function, which is called during onMouseDown. However, it doesn't really do anything. I tried having that and then typing
noteRef.current.style.zIndex = 1
inside the handleOnUp function, and while I was able to have the selected note on top of the others while moving, obviously it just goes right back below the notes when I release the mouse.
I've also tried using useEffect but it also didn't change anything. I was wondering if there is a way to access functions from the App component (where the note components reside).
**EDITED
Here is my example code sandbox.
How about managing notes` z-index with state in parent component?
In below's my example, i used useState in App component and made stackNote function for handling child component's style.
function App () {
const [zIndex, setZindex] = useState(1);
const stackNote = (ref) => {
ref.current.style.zIndex = zIndex;
setZindex((zIndex) => zIndex + 1);
};
return (
<div>
<Header />
{notes.map((noteItem) => (
<Note
stackNote={stackNote}
key={noteItem.key}
title={noteItem.title}
content={noteItem.content}
/>
))}
<Footer />
</div>
);
}
Then, in the Note component, just call stackNote with noteRef in your handleOnClick.
function handleOnClick(e) {
e.preventDefault();
props.stackNote(noteRef);
offsetX = e.pageX - notePos.x;
offsetY = e.pageY - notePos.y;
console.log("Mouse is clicked!");
document.addEventListener("mousemove", handleOnMove);
document.addEventListener("mouseup", handleOnUp);
}
And don't forget should erase previous codes something like noteRef.current.style.zIndex = ...
Although ref existed in the original code, so i used as it is, but it doesn't seem necessary to use it.

Resources