why useRef current value , isn't sharing trough custom hook? - reactjs

I wanted to calculate the user scroll height , so I created a custom hook. and I wanted to share this value to another component. but it doesnt work.
code:
const useScroll = () => {
let scrollHeight = useRef(0);
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", () => {});
};
}, []);
return scrollHeight.current;
};
export default useScroll;
the value is not updating here.
but if I use useState here , it works. but that causes tremendous amount of component re-rendering. can you have any idea , how its happening?

Since the hook won't rerender you will only get the return value once. What you can do, is to create a useRef-const in the useScroll hook. The useScroll hook returns the reference of the useRef-const when the hook gets mounted. Because it's a reference you can write the changes in the useScroll hook to the useRef-const and read it's newest value in a component which implemented the hook. To reduce multiple event listeners you should implement the hook once in the parent component and pass the useRef-const reference to the child components. I made an example for you.
The hook:
import { useCallback, useEffect, useRef } from "react";
export const useScroll = () => {
const userScrollHeight = useRef(0);
const scroll = useCallback(() => {
userScrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
}, []);
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", scroll);
};
}, []);
return userScrollHeight;
};
The parent component:
import { SomeChild, SomeOtherChild } from "./SomeChildren";
import { useScroll } from "./ScrollHook";
const App = () => {
const userScrollHeight = useScroll();
return (
<div>
<SomeChild userScrollHeight={userScrollHeight} />
<SomeOtherChild userScrollHeight={userScrollHeight} />
</div>
);
};
export default App;
The child components:
export const SomeChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "aqua"
}}>
<h1>SomeChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};
export const SomeOtherChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeOtherChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "orange"
}}>
<h1>SomeOtherChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};

import { useRef } from 'react';
import throttle from 'lodash.throttle';
/**
* Hook to return the throttled function
* #param fn function to throttl
* #param delay throttl delay
*/
const useThrottle = (fn, delay = 500) => {
// https://stackoverflow.com/a/64856090/11667949
const throttledFn = useRef(throttle(fn, delay)).current;
return throttledFn;
};
export default useThrottle;
then, in your custom hook:
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
const throttledScroll = useThrottle(scroll)
Also, I like to point out that you are not clearing your effect. You should be:
useEffect(() => {
window.addEventListener("scroll", throttledScroll);
return () => {
window.removeEventListener("scroll", throttledScroll); // remove Listener
};
}, [throttledScroll]); // this will never change, but it is good to add it here. (We've also cleaned up effect)

Related

React + fabric.js

I am trying to combine react and fabricjs but I am stuck.
Here is my code
import React, { useState, useEffect, useRef } from 'react';
import { fabric } from "fabric";
function App() {
const [canvas, setCanvas] = useState('');
useEffect(() => {
setCanvas(initCanvas());
}, []);
const initCanvas = () => (
new fabric.Canvas('canvas', {
height: 800,
width: 800,
backgroundColor: 'pink' ,
selection: false,
renderOnAddRemove: true,
})
)
canvas.on("mouse:over", ()=>{
console.log('hello')
})
return (
<div >
<canvas id="canvas" />
</div>
);
}
export default App;
The problem is canvas.on as it causes the error 'Uncaught TypeError: canvas.on is not a function'
Please tell me what am I doing wrong here
During the initial render, your canvas variable is set to your initial state, '' from useState(''). It's not until after this that your useEffect will run, updating the state value.
Recommendation: Move your event handlers into the useEffect and use a ref instead of state for your canvas value. refs have the property of being directly mutable and not requiring a rerender for their new value to be available.
import React, { useState, useEffect, useRef } from 'react';
import { fabric } from "fabric";
function App() {
const canvas = useRef(null);
useEffect(() => {
canvas.current = initCanvas();
canvas.current.on("mouse:over", () => {
console.log('hello')
});
// destroy fabric on unmount
return () => {
canvas.current.dispose();
canvas.current = null;
};
}, []);
const initCanvas = () => (
new fabric.Canvas('canvas', {
height: 800,
width: 800,
backgroundColor: 'pink' ,
selection: false,
renderOnAddRemove: true,
})
);
return (
<div >
<canvas ref={canvas} />
</div>
);
}
export default App;
It's worth noting that if you don't need a reference to the canvas elsewhere in your component, you don't need to use state or a ref and can use a local variable within the useEffect.
useEffect(() => {
const canvas = initCanvas();
canvas.on("mouse:over", () => {
console.log('hello')
});
// destroy fabric on unmount
return () => {
canvas.dispose();
};
})
Actually the problem is that you trying to call canvas.on when it is an empty string in canvas (initial state)
Since we are only need to create fabric.Canvas once, I would recommend to store instance with React.useRef
I created an example for you here:
--> https://codesandbox.io/s/late-cloud-ed5r6q?file=/src/FabricExample.js
Will also show the source of the example component here:
import React from "react";
import { fabric } from "fabric";
const FabricExample = () => {
const fabricRef = React.useRef(null);
const canvasRef = React.useRef(null);
React.useEffect(() => {
const initFabric = () => {
fabricRef.current = new fabric.Canvas(canvasRef.current);
};
const addRectangle = () => {
const rect = new fabric.Rect({
top: 50,
left: 50,
width: 50,
height: 50,
fill: "red"
});
fabricRef.current.add(rect);
};
const disposeFabric = () => {
fabricRef.current.dispose();
};
initFabric();
addRectangle();
return () => {
disposeFabric();
};
}, []);
return <canvas ref={canvasRef} />;
};
export default FabricExample;

React state doesnt update, drilling seems ok though

I've got a modal that I want to be able to auto-shut itself using a drilled function. The console.log does work, but the state isn't actually updating. What am I doing wrong? Triggering the state via the dev tools works fine, so it's not the state itself. Is drilling within a component the problem?
index.js:
export default function Home() {
const [modalOpen, setModalOpen] = useState(false)
const handleModalOpen = () => {
console.log ("Setting modal to true")
setModalOpen (true)
}
const handleModalClose = () => {
console.log ("Setting modal to false")
setModalOpen (false)
}
// all the normal app body
{modalOpen ?
(<Modal handleModalClose={handleModalClose} height='30vh'>
<h4>Thank you for your contact request.</h4>
<h4>Keep in mind that this is only a demo website, not an actual business.</h4>
</Modal>): null}
</div>
)
}
Modal.js:
import { createPortal } from "react-dom";
import { useEffect, useState } from "react";
import styles from '../styles/Modal.module.css'
const Backdrop = (props) => {
return <div onClick={() => props.handleModalClose()} className={styles.backdrop} />
}
const Message = (props) => {
let width = '70vw'
let height = '80vh'
if (props.width) width = props.width
if (props.height) height = props.height
return (
<div style={{ width: width, height: height }} className={styles.message}>
{props.children}
</div>
)
}
const Modal = (props) => {
const [backdropDiv, setBackdropDiv] = useState(null)
const [modalDiv, setModalDiv] = useState(null)
useEffect(() => {
if (typeof (window) !== undefined) {
let backdropDiv = document.getElementById('backdrop')
setBackdropDiv(backdropDiv)
let modalDiv = document.getElementById('modal')
setModalDiv(modalDiv)
}
}, [])
return (
<>
{backdropDiv !== null && modalDiv !== null ? (
<>
{createPortal(<Backdrop handleModalClose = {props.handleModalClose} />, backdropDiv)}
{createPortal(<Message children={props.children} width={props.width} height={props.height} />, modalDiv)}
</>
) : null
}
</>
)
}
export default Modal

Component starts to break after some time - react

I want to create a component that will take any image and make it spin in circles.
I managed to do so but appears I have an issue with setting the interval cleanup function as it starts to switch quickly from one state to another and the picture spins like crazy.
This is the spinner component
import classes from './Spinner.module.css'
import { useState , useEffect} from 'react';
const Spinner = (props) =>{
const [Timer, setTimer] = useState('5');
useEffect(() => {
const interval = setInterval(() => {
let newT;
if(Timer==='5'){
newT='1';
}
else{
newT='5';
}
setTimer(newT);
}, 2000);
console.log(Timer);
return clearInterval(interval);
}, [Timer])
return <img style={{animation: `${classes.spin} ${Timer}s linear infinite`}} src={props.img} alt="img"/>
};
export default Spinner ;
Spin CSS :
#keyframes spin {
from {transform:rotate(0deg);}
to {transform:rotate(360deg);}
}
When the interval created cleanup function it did not work, since it was not in a function.
Change was in the return function of the useEffect component.
const Spinner = (props) => {
const [Timer, setTimer] = useState("5");
useEffect(() => {
const interval = setInterval(() => {
setTimer((prevState) => prevState ==='5' ? '1' : '5');
}, 2000);
console.log(Timer);
return ()=>{clearInterval(interval)};
}, [Timer]);
return (
<img
style={{ animation: `${classes.spin} ${Timer}s linear infinite` }}
src={props.img}
alt="img"
/>
);
};

Testing slider component in react

I want to test my slider component with react testing library. But I can't comprehend how to test it properly. I want to test changing slide when the user clicks the dot(StyledDotContainer). StyledDotContainer's background is gray but it is red when the active props is true. The component looks like this.
const Slider = ({
children,
autoPlayTime = 5000,
dots = true,
initialIndex= 0
}: SliderProps): React.ReactElement => {
const [activeIndex, setActiveIndex] = useState<number>(initialIndex)
const nextSlide = () => {
const newIndex = activeIndex >= length - 1 ? 0 : activeIndex + 1
setActiveIndex(newIndex)
}
useEffect(() => {
const timer = setTimeout(() => {
nextSlide()
}, autoPlayTime)
return () => clearTimeout(timer)
}, [activeIndex])
const length = useMemo(() => {
return React.Children.count(children)
}, [])
const setSlide = useCallback((index: number) => {
setActiveIndex(index)
}, [])
const value = useMemo(() => ({ activeIndex, setSlide }), [activeIndex])
return (
<SliderContext.Provider value={value}>
<StyledContainer>
{children}
{dots && (
<StyledDotsContainer data-testid="dots">
{[...Array(length)].map((_, index) => {
return (
<StyledDotContainer
data-testid={`dot-${index}`}
key={index}
onClick={() => setSlide(index)}
isActive={index === activeIndex}
/>
)
})}
</StyledDotsContainer>
)}
</StyledContainer>
</SliderContext.Provider>
)
}
Appreciate any suggestion.
It's a bad practice to test the styles of a Component. Instead you just want to test that the function you are trying to test is properly changing the props in your component. Styles should be inspected visually.
import screen from '#testing-library/dom'
import {render, screen} from '#testing-library/react'
import userEvent from '#testing-library/user-event'
describe("Slider", () => {
it('Should be red when slider is active', () => {
render(Slider)
const firstDot = screen.getByTestId('dots-0')
act(() => {
userEvent.click(firstDot)
})
waitFor(() => {
expect(screen.getByTestId('dots-0').props.isActive).toBeTruthy()
})
})
})

When accessing a state variable from a useCallback, value is not updated

At a certain place of my code I'm accessing a state variable of my component from a call back ( UserCallback ) and I find the state variable has not updated from the initial value and call back is referring to the initial value. As I read in the documentation when variable is passed as one of array items then it should update the function when it is updated. Following is a sample code.
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
const node = useRef(null);
useImperativeHandle(ref, () => ({
increment() {
setCount(count + 1);
}
}));
const clickListener = useCallback(
e => {
if (!node.current.contains(e.target)) {
alert(count);
}
},
[count]
);
useEffect(() => {
// Attach the listeners on component mount.
document.addEventListener("click", clickListener);
// Detach the listeners on component unmount.
return () => {
document.removeEventListener("click", clickListener);
};
}, []);
return (
<div
ref={node}
style={{ width: "500px", height: "100px", backgroundColor: "yellow" }}
>
<h1>Hi {count}</h1>
</div>
);
});
const Parent = () => {
const childRef = useRef();
return (
<div>
<Child ref={childRef} />
<button onClick={() => childRef.current.increment()}>Click</button>
</div>
);
};
export default function App() {
return (
<div className="App">
<Parent />
</div>
);
}
What I'm originally building is a custom confirmation modal. I have a state variable which set either display:block or display:none to the root element. Then if there is a click outside the component I need to close the modal by setting state variable to false. Following is the original function.
const clickListener = useCallback(
(e: MouseEvent) => {
console.log('isVisible - ', isVisible, ' count - ', count, ' !node.current.contains(e.target) - ', !node.current.contains(e.target))
if (isVisible && !node.current.contains(e.target)) {
setIsVisible(false)
}
},
[node.current, isVisible],
)
It doesn't get closed because isVisible is always false which is the initial value.
What am I doing wrong here?
For further clarifications following is the full component.
const ConfirmActionModal = (props, ref) => {
const [isVisible, setIsVisible] = useState(false)
const [count, setCount] = useState(0)
const showModal = () => {
setIsVisible(true)
setCount(1)
}
useImperativeHandle(ref, () => {
return {
showModal: showModal
}
});
const node = useRef(null)
const stateRef = useRef(isVisible);
const escapeListener = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') {
setIsVisible(false)
}
}, [])
useEffect(() => {
stateRef.current = isVisible;
}, [isVisible]);
useEffect(() => {
const clickListener = e => {
if (stateRef.current && !node.current.contains(e.target)) {
setIsVisible(false)
}
};
// Attach the listeners on component mount.
document.addEventListener('click', clickListener)
document.addEventListener('keyup', escapeListener)
// Detach the listeners on component unmount.
return () => {
document.removeEventListener('click', clickListener)
document.removeEventListener('keyup', escapeListener)
}
}, [])
return (
<div ref={node}>
<ConfirmPanel style={{ display : isVisible ? 'block': 'none'}}>
<ConfirmMessage>
Complete - {isVisible.toString()} - {count}
</ConfirmMessage>
<PrimaryButton
type="submit"
style={{
backgroundColor: "#00aa10",
color: "white",
marginRight: "10px",
margin: "auto"
}}
onClick={() => {console.log(isVisible); setCount(2)}}
>Confirm</PrimaryButton>
</ConfirmPanel>
</div>
)
}
export default forwardRef(ConfirmActionModal)
You assign a function clickListener to document.addEventListener on component mount, this function has a closure on count value.
On the next render, the count value will be stale.
One way to solve it is implementing a function with refernce closure instead:
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
// countRef.current always holds the most updated state
const clickListener = e => {
if (!node.current.contains(e.target)) {
alert(countRef.current);
}
};
document.addEventListener("click", clickListener);
return () => {
document.removeEventListener("click", clickListener);
};
}, []);
...
}
You can pass a callback to setIsvisible so you don't need isVisible as a dependency of the useCallback. Adding node.current is pointless since node is a ref and gets mutated:
const clickListener = useCallback((e) => {
setIsVisible((isVisible) => {//pass callback to state setter
if (isVisible && !node.current.contains(e.target)) {
return false;
}
return isVisible;
});
}, []);//no dependencies needed
While your clickListener does change when count changes you only bind the initial clickListener once on mount because your useEffect dependency list is empty. You could ad clickListener to the dependency list as well:
useEffect(() => {
// Attach the listeners on component mount.
document.addEventListener("click", clickListener);
// Detach the listeners on component unmount.
return () => {
document.removeEventListener("click", clickListener);
};
}, [clickListener]);
Side note: using node.current in a dependency list doesn't do anything as react does not notice any changes to a ref. Dependencies can only be state or props.

Resources