How to change z index of components in React? - reactjs

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.

Related

React event listener callback functions don't use updated states

When accessed from reDraw the resizableList is an empty array, when accessed from addImageClick it shows the actual array. the TextInput element contains a text input which calls eventBus.dispatch('addtext') on change.
So, after I add an image, I have two TextInput elements, and two Resizable elements. I change the text in one of the TextInput elements, empty state array. I trigger the add image button which logs the array before resetting it, array has two elements in it before reset.
import React,{useState,useRef,useEffect} from "react";
import Resizable from "./Resizable";
import TextInput from "./TextInput";
import eventBus from "../eventbus/EventBus";
export default function Meme(){
const [image, setImage] = useState(new Image());
const [resizableList, setResizableList] = useState([]);
const hiddenFileInput = useRef(null);
const cvs = useRef(null);
function reDraw(){
const width = 600*(image.width/image.height);
const ctx = cvs.current.getContext("2d");
ctx.clearRect(0, 0, 1200, 600);
ctx.drawImage(image,(1200-(width))/2,0,width,600);
console.log(resizableList);
for (let i=0;i<resizableList.length;i++){
let el = document.getElementById(`${i}--textbox`);
console.log('test');
}
console.log('ran');
console.log(image);
}
function addResizable(width){
let resizable = {
cvs:cvs,
imgwidth:width,
image:image
}
setResizableList((prevResizableList)=>[...prevResizableList, resizable]);
}
function draw() {
const width = 600*(image.width/image.height);
const ctx = cvs.current.getContext("2d");
console.log(image.width);
ctx.clearRect(0, 0, 1200, 600);
ctx.drawImage(image,(1200-(width))/2,0,width,600);
addResizable(width);
addResizable(width);
};
function addImageClick(e){
e.preventDefault();
console.log(resizableList);
setResizableList([]);
hiddenFileInput.current.click();
};
useEffect(()=>{
eventBus.on('addtext',reDraw);
image.onload = draw;
console.log('useeffect');
},[])
return (
<main>
<form className="form">
<div className='form--textboxes'>
{resizableList.map((item,index)=>{
return (<TextInput cvs={item.cvs} image={item.image} imgwidth={item.imgwidth} key={index} id={index}/>)
})}
<button className='form--button--textboxes'>Add Text</button>
</div>
<button className="form--button" onClick={addImageClick}>Add Image</button>
<div className="form--image">
<canvas id='meme' ref={cvs} className="form--meme" height="600" width="1200"></canvas>
{resizableList.map((item,index)=>{
return (<Resizable cvs={item.cvs} imgwidth={item.imgwidth} key={index} id={index}/>)
})}
</div>
</form>
<input
type="file"
name="myImage"
style={{display:'none'}}
ref={hiddenFileInput}
onChange={(event) => {
image.src = URL.createObjectURL(event.target.files[0]);
}}
/>
</main>
)
};
I apologize if the explanation above is too long. So far I've been able to get by just by reviewing questions, it's the first time I actually had to write one.
Update: What I basically want to do is loop over the resizableList array, and update the text on the canvas based on the position and size of each resizable. I know how to do that but I can't, because when I access the array from the reDraw function, the array shows as empty. I tried looking for alternatives but I couldn't find any. I create an id for each textbox/resizable of format: arrayindex--textbox and arrayindex--box. That's how the textboxes and resizables relate to eachother.
2nd Update: Sooooooo...
function addTextClick(e){
e.preventDefault();
console.log(resizableList);
}
function reDraw(){
const width = 600*(image.width/image.height);
const ctx = cvs.current.getContext("2d");
ctx.clearRect(0, 0, 1200, 600);
ctx.drawImage(image,(1200-(width))/2,0,width,600);
console.log(resizableList);
button.current.click();
}
As I said previously, reDraw is triggered by an event fired by the text input's onChange. I also made this button which has the addTextClick as it's onClick. And yes, I trigger input's onchange, I get one empty array followed by one length 2 array(which is the resizableList array) in my console. ?????
I guess this kinda fixes my problem as I can just create a hidden button but this is more of a workaround and I would still like to understand why this is happening.
Updated with solution:
So, I ran into this same issue while working on some other functionality of my webapp.
The problem is that when you add an event listener the callback function is set with the state values present at the time. So it doesn't matter how many times your states are updated after setting the event listener, it's still going to use the states it had when it was set up.
Funny enough, after understanding what was causing the problem I found some posts from others running into this problem and using exactly the same workaround I used above, which is to create a reference to a hidden button and have the event listener's callback function trigger the onClick.
Another solution is to use the useEffect hook and recreate the event listener every time a state is updated, but in my case that doesn't really work that well since I have a lot of states that are updated constantly.

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

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.

Why does using React Context with Framer Motion not work?

I have a version of a slideshow where the state is being stored locally, you can see that the slideshow works great and the slide components only are unmounted once the animation is complete. https://stackblitz.com/edit/react-framer-motion-slideshow-official?file=src%2FSlideShow.js
Once I added the context to handle the values, the animation sliding still works but the exiting component is replaced with the new slide content when the animation begins, which looks really strange. Also the custom value for the slide directions seems to be broken. https://stackblitz.com/edit/react-framer-motion-slideshow-official-context?file=src%2FSlideShow.js
Do you have any ideas how I can get the animation to work correctly again when using context?
Everything that consumes a context re-renders every time that context’s state changes. So the children of your Slides component
rerender
see that the new variant = to the next state
appear at the destination
If I were you I wouldn't use context. If you really want to not explicitly pass the same props over and over you can do
{[
MainSettingsSlide,
ChangeLanguageSlide,
LanguageDetailsSlide,
BlockedSitesSlide
].map((Component, i) => (
<Component
activeSlideName={activeSlideName}
onNavigateSidebar={onNavigateSidebar}
key={i}
/>
))}
Sorry for the indirect answer :)
Edit two days later
In rereading your question I realize there are some other problems
You need to always conditionally render based on props not context
const Slide = ({ children, slideName, className, activeSlideName }) => {
// This context will update outside of framer-motion
// framer-motion animating something in while it is animating something out is
// predicated on you giving it control by using props
// const { activeSlideName } = useSlideShowContext();
// console.log('activeSlideName in Slide', activeSlideName);
// console.log('---------------------');
if (activeSlideName !== slideName) {
return null;
}
Your onNavigateSlideShow was using slideDirection instead of direction
const onNavigateSlideShow = ({ slide, direction = 'forward' }) => {
// const onNavigateSlideShow = ({ slide, slideDirection = 'forward' }) => {
console.log('ccc', direction);
setActiveSlide([slide, direction]);
};
I still can't get the directions to go in the right direction
I think this is due to a race condition between the direction being set and when the animation is kicked off
If you click the back button before the animation completes it works as expected.
Here is where I got to: https://stackblitz.com/edit/react-framer-motion-slideshow-official-context-dftoab?file=src/SlideShow.js
Sorry that this is again not a complete answer. I think I am coming to the same conclusion as before that the two apis probably shouldn't be mixed. Especially due to edge cases like this one.
There have been a decent number of questions recently about context and AnimatePresence so made sandbox for most of the cases that I could think of: https://codesandbox.io/s/framer-motion-using-context-with-animate-presensce-nuj0m

How to create a resizable component in React

As said in the title. I want to create a React component that will give me a possibility to resize its width by dragging - just like windows in Windows operating system. What is actually the best approach to handle this issue ?
EDIT
I included my current approach to the subject of the matter:
First I placed a "dragger" element in the top-right corner of my container. When i press mouse down on that element i want to create a mousemove event listener which will modify the containerWidth in respect to the X coordinate of the cursor relative to the initial X position of the edge of the container. I already have that event listener firing and logging me the coordinates after holding down the mouse button but unfortunatelly for some reason the event is not being removed after the mouse is unpressed(mouseUp event) which is not what i intended. Any suggestions appreciated, also those about some issues i might expect in the future related to this topic. Thanks.
type Props = MasterProps & LinkStateToProps & LinkDispatchToProps;
const Test3 = (Props: Props) => {
const [containerWidth, setContainerWidth] = React.useState(640)
const [isBeingStretched, setIsBeingStretched] = React.useState(false);
const masterRef = React.useRef(null);
const logMousePosition = React.useCallback((event:MouseEvent)=>{
console.log(event.clientX);
},[])
const handleMouseDown=()=>{
document.addEventListener('mousemove', logMousePosition);
masterRef.current.addEventListener('mouseup', ()=>{
document.removeEventListener('mouseup', logMousePosition)
})
}
const handleMouseUp = () => {
document.removeEventListener('mousemove', logMousePosition);
}
return (
<div className="master-wrapper" ref={masterRef}>
<div className="stretchable-div" style={{ width: `${containerWidth}px` }}>
<div className="dragger-wrapper">
<h2>This is supposed to change width</h2>
<div className="dragger"
id="dragger"
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}/>
</div>
</div>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(Test3);
I'd never done something like this before so I decided to give it a go, and it ended up being quite straightforward to implement with React state management. I can see why you might not know where to start if you are new to React, and that's ok, although two things to note before I go through my solution:
Statements such as document.getElementById or document.addEventListener are not going to function as intended anymore. With React, you are manipulating a virtual DOM, which updates the actual DOM for you, and you should aim to let it do that as much as possible.
Using refs to get around this fact is bad practice. They may act in a similar way to the statements mentioned above but that is not their intended use case. Read up on what the documentation has to say about good use cases for ref.
Here's what the JSX portion of my demo looks like:
return (
<div className="container" onMouseMove={resizeFrame} onMouseUp={stopResize}>
<div className="box" style={boxStyle}>
<button className="dragger" onMouseDown={startResize}>
Size Me
</button>
</div>
</div>
);
We're going to need three different events - onMouseDown, onMouseMove and onMouseUp - to track the different stages of the resize. You already got this far in your own code. In React, we declare all these as attributes of the elements themselves, although they are not actually in-line functions. React adds them as event listeners for us in the background.
const [drag, setDrag] = useState({
active: false,
x: "",
y: ""
});
const startResize = e => {
setDrag({
active: true,
x: e.clientX,
y: e.clientY
});
};
We'll use some state to track the resize as it is in progress. I condensed everything into a single object to avoid bloat and make it more readable, although this won't always be ideal if you have other hooks like useEffect or useMemo dependent on that state. The first event simply saves the initial x and y positions of the user's mouse, and sets active to true for the next event to reference.
const [dims, setDims] = useState({
w: 200,
h: 200
});
const resizeFrame = e => {
const { active, x, y } = drag;
if (active) {
const xDiff = Math.abs(x - e.clientX);
const yDiff = Math.abs(y - e.clientY);
const newW = x > e.clientX ? dims.w - xDiff : dims.w + xDiff;
const newH = y > e.clientY ? dims.h + yDiff : dims.h - yDiff;
setDrag({ ...drag, x: e.clientX, y: e.clientY });
setDims({ w: newW, h: newH });
}
};
The second piece of state will initialise and then update the dimensions of the element as its values change. This could use any measurement you want although it will have to correlate to some CSS property.
The resizeFrame function does the following:
Make the properties of drag easily available via destructuring assignment. This will make the code more readable and easier to type.
Check that the resize is active. onMouseMove will fire for every pixel the mouse moves over the relevant element so we want to make sure it is properly conditioned.
Use Math.abs() to get the difference in value between the current mouse position and the saved mouse position as a positive integer. This will save us from having to do a second round of conditional statements.
Use turnary statements to either add or subtract the difference from the dimensions, based on whether the new mouse position is greater or less than the previous on either axis.
Set the states with the new values, using the spread operator ... to leave the irrelevant part of drag as it is.
const stopResize = e => {
setDrag({ ...drag, active: false });
};
const boxStyle = {
width: `${dims.x}px`,
height: `${dims.y}px`
};
Then we simply set the activity of the drag state to false once the user is finished. The JS style object is passed to the element with the state variable in place so that is taken care of automatically.
Here is the codesandbox with the finished effect.
One of the drawbacks to doing things this way is that it basically requires you to have that mouseMove event listener assigned to the largest site container, because the mouse is not going to stay within the bounds of the box during the resize. That could be an issue if you want to have multiple elements with the same functionality, although nothing that you can't solve with good state management. You could probably fine tune this so that the mouse always stays on the drag element, although that would require a more complex implementation.

Is it safe to run animations in a useEffect hook?

I've started introducing some React hooks into my code, specifically the useEffect and I can't seem to find out whether what I'm doing is considered safe or not. Essentially I'm running animations on the DOM within the hook, and I want to ensure that's not going to break any DOM snapshots for example.
Here's an example, I've modified from my full example to try and be concise to illustrate what's happening:
export function GrowingCircle(props) {
const root = useRef(null); // This is the root element we draw to
// The actual rendering is done whenever the data changes
useEffect(() => {
const radius = props.width / 2;
d3.select(root.current)
.transition()
.duration(1000)
.attr("r", radius);
}, [props.width]);
return (
<svg width={props.width} height="100%">
<circle ref={root} cx="0" cy="0" r="0" fill="red" />
</svg>
);
}
The part I'm concerned about is the .transition() is going to run frequent updates on the DOM for 1 second, and I'm unsure if that's going to mess up the react rendering?
A follow up question (as often we don't have control of the animation rendering like in this example). Would the following where the circle is no longer within the JSX change things?
export function GrowingCircle(props) {
const root = useRef(null); // This is the root element we draw to
// The actual rendering is done whenever the data changes
useEffect(() => {
const radius = props.width / 2;
d3.select(root.current)
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("fill", "red")
.transition()
.duration(1000)
.attr("r", radius);
}, [props.width]);
return (
<svg ref={root} width={props.width} height="100%">
</svg>
);
}
I have been in your same scenario, to give you a short answer, since there is no real D3 - React implementation, you need to draw your own boundaries of imperative versus declarative rendering. However in both your example, you aren't really breaking any rules.
In your second example you're passing the reins to D3 to do the full rendering, while React simple keeps a ref to the top-level svg element. While in your first example, you're rendering a single circle, and only managing its transition with D3.
However in the 1st example, since it looks like you never change anything in the DOM after declaration, React should geneally never interfere. Here's a third example achieving something similar to what you wrote :
export function GrowingCircle(props) {
const root = useRef(null); // This is the root element we draw to
const [radius,setRadius] = useState(0); //initial radius value
useEffect(() => {
d3.select(root.current)
.transition()
.duration(1000)
.attr("r", props.width / 2)
.on("end", () => {setRadius(props.width / 2)}); //this is necessary
}, [props.width]);
return (
<svg width={props.width} height="100%">
<circle ref={root} cx="0" cy="0" r={radius} fill="red" />
</svg>
);
}
I've ran into a similar problem myself, and from what I understood, during the transition d3 acts on your desired value directly, causing it not to trigger react's re-render mechanism. But once it's done, I'm assuming it directly tries to act on the radius value, and your circle snaps back to its 0 original value. So you simply add an .on('end') event to update its state, and there you have it!
I feel like this is as close as it gets to the react way, where d3 only selects elements rendered based on react state.
DOM elements rendered by React can be modified. However if React needs to re-render because the Virtual DOM doesn't match with the current DOM (the one that React has as the current) it might replace the modified parts, and changes made out of React could be lost.
React modifies the necessary parts so modifications might remain if React only modified a part of the element or they might be removed. So, as far as I understand, we can not trust modifications will remain. Only if we knew for sure the Component output will not change after the custom modifications.
My suggestion is to use React to keep track of any change:
Use useState and useEffect to modify style properties.
Use CSS classes and handle the animation using CSS.
Use an animation library which is made for React (take a look at Framer motion).

Resources