How to test useRef with Jest and react-testing-library? - reactjs

I'm using create-react-app, Jest and react-testing-library for the configuration of the chatbot project.
I have a functional component that uses useRef hook. When a new message comes useEffect hook is triggered and cause scrolling event by looking a ref's current property.
const ChatBot = () => {
const chatBotMessagesRef = useRef(null)
const chatBotContext = useContext(ChatBotContext)
const { chat, typing } = chatBotContext
useEffect(() => {
if (typeof chatMessagesRef.current.scrollTo !== 'undefined' && chat && chat.length > 0) {
chatBotMessagesRef.current.scrollTo({
top: chatMessagesRef.current.scrollHeight,
behavior: 'smooth'
})
}
// eslint-disable-next-line
}, [chat, typing])
return (
<>
<ChatBotHeader />
<div className='chatbot' ref={chatBotMessagesRef}>
{chat && chat.map((message, index) => {
return <ChatBotBoard answers={message.answers} key={index} currentIndex={index + 1} />
})}
{typing &&
<ServerMessage message='' typing isLiveChat={false} />
}
</div>
</>
)
}
I want to be able to test whether is scrollTo function triggered when a new chat item or typing comes, do you have any ideas? I couldn't find a way to test useRef.

You can move your useEffect out of your component and pass a ref as a parameter to it. Something like
const useScrollTo = (chatMessagesRef, chat) => {
useEffect(() => {
if (typeof chatMessagesRef.current.scrollTo !== 'undefined' && chat && chat.length > 0) {
chatBotMessagesRef.current.scrollTo({
top: chatMessagesRef.current.scrollHeight,
behavior: 'smooth'
})
}
}, [chat])
}
Now in your component
import useScrollTo from '../..'; // whatever is your path
const MyComponent = () => {
const chatBotMessagesRef = useRef(null);
const { chat } = useContext(ChatBotContext);
useScrollTo(chatBotMessagesRef, chat);
// your render..
}
Your useScrollTo test:
import useScrollTo from '../..'; // whatever is your path
import { renderHook } from '#testing-library/react-hooks'
it('should scroll', () => {
const ref = {
current: {
scrollTo: jest.fn()
}
}
const chat = ['message1', 'message2']
renderHook(() => useScrollTo(ref, chat))
expect(ref.current.scrollTo).toHaveBeenCalledTimes(1)
})

Related

ReactJs : useRef current getting null

I am trying to turnoff camera and flashlight when the component gets unmount , I am using react hook I have two function startCamera() and stopCamera() , I am calling startcamera when the component gets mount and stop camera when component gets unmount.
But its showing me error when stopCamera is called while unmounting
i have created an another button to test if StopCamera() working and i found its working , but i want to call the function when component is getting unmounted
my code:
CameraScreen.js
import "./styles.css";
import { useState, useEffect, useRef } from "react";
export default function CameraScreen() {
const videoElement = useRef(null);
const [facingMode, setFacingMode] = useState("environment");
const handleFacingModeToggle = () => {
stopCamera();
facingMode === "environment"
? setFacingMode("user")
: setFacingMode("environment");
};
useEffect(() => {
// const getUserMedia = async () => {
// try {
// const stream = await navigator.mediaDevices.getUserMedia({
// video: true
// });
// videoElement.current.srcObject = stream;
// } catch (err) {
// console.log(err);
// }
// };
// getUserMedia();
startCamera();
return function cleanup() {
stopCamera();
};
}, []);
const stopCamera = () =>
videoElement.current.srcObject &&
videoElement.current.srcObject.getTracks().forEach((t) => t.stop());
function startCamera() {
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({
video: { facingMode: facingMode },
width: { ideal: 1280 },
height: { ideal: 720 }
})
.then(function (stream) {
if (videoElement.current) videoElement.current.srcObject = stream;
const track = stream.getVideoTracks()[0];
//Create image capture object and get camera capabilities
const imageCapture = new ImageCapture(track);
const photoCapabilities = imageCapture
.getPhotoCapabilities()
.then(() => {
//todo: check if camera has a torch
//let there be light!
track.applyConstraints({
advanced: [{ torch: true }]
});
});
})
.catch(function (error) {
alert("Please check your device permissions");
console.log("Something went wrong!");
console.log(error);
});
if (videoElement.current)
videoElement.current.onloadeddata = function () {
if (window.NativeDevice)
window.NativeDevice.htmlCameraReadyToRecord(true);
};
}
}
return (
<>
<video
autoPlay={true}
ref={videoElement}
style={{
minHeight: "67.82vh",
maxHeight: "67.82vh",
maxWidth: "100%",
minWidth: "100%"
}}
className="border-3rem bg-[#666]"
></video>
<button onClick={stopCamera}> stopCamera</button>
</>
);
}
App.js
import "./styles.css";
import { useState } from "react";
import CameraScreen from "./cameraScreen";
export default function App() {
const [switchS, setSwitchS] = useState(false);
return (
<div>
<button className="" onClick={() => setSwitchS(!switchS)} value="switch">
switch
</button>
{switchS && <CameraScreen />}
{!switchS && "Blank Screen"}
</div>
);
}
PS: the above code working at :https://5t2to.csb.app/
codesandbox link : https://codesandbox.io/s/practical-fast-5t2to?file=/src/cameraScreen.js
You can use useLayoutEffect hook. It works just before unmounting, like componentWillUnmount.
Here is an example to that
https://codesandbox.io/s/happy-swartz-ikqdn?file=/src/random.js
You can go to https://ikqdn.csb.app/rand in sandbox browser and check the console on clicking to home button.
You can see the difference in working while unmounting of both useEffect and useLayoutEffect
It preserves ref.current, so what you can do is, you can pass ref.current in the function that you are calling just before unmounting, to prevent ref to the dom elment.
It took a bit of debugging/sleuthing to find the issue. So even though you have a ref attached to the video element, when the component is unmounted the ref is still mutated and becomes undefined. The solution is to save a reference to the current videoElement ref value and use this in a cleanup function.
useEffect(() => {
startCamera();
const ref = videoElement.current;
return () => {
ref.srcObject.getTracks().forEach((t) => t.stop());
};
}, []);
Simply add useLayoutEffect to stop camera
useEffect(() => {
// const getUserMedia = async () => {
// try {
// const stream = await navigator.mediaDevices.getUserMedia({
// video: true
// });
// videoElement.current.srcObject = stream;
// } catch (err) {
// console.log(err);
// }
// };
// getUserMedia();
startCamera();
}, []);
useLayoutEffect(()=>()=>{
stopCamera();
},[]);
Just need to change useEffect() to useLayoutEffect()and it will works like a charm.
useLayoutEffect(() => {
const ref = videoElement;
console.log(ref);
startCamera();
return function cleanup() {
stopCamera();
};
}, []);
sanbox link :- https://codesandbox.io/s/naughty-murdock-by5tc?file=/src/cameraScreen.js:415-731

In React, how do you apply componentDidMount for this type of component?

I have a React 16 application. I have a component constructed like so ...
const AddressInput = (props) => {
  const classes = useStyles();
  const { disabled, error, location, onClear, placeholder, setLocation, showMap, value } = props;
useEffect(() => {
console.log("use effect 1 ...");
if (value && placesRef?.current) {
placesRef.current.setVal(value);
if (zoom === 0) {
setZoom(16);
}
}
}, [placesRef, value, disabled, zoom]);
useEffect(() => {
console.log("use effect 2 ...");
if (location) {
if (zoom === 0) {
setZoom(16);
}
if (location.lat !== position[0]) {
setPosition([location.lat, location.lng]);
}
}
}, [location, zoom, position]);
useEffect(() => {
console.log("use effect 3 ...");
console.log("setting " + inputId + " to :" + placeholder);
if (document.getElementById(inputId)) {
document.getElementById(inputId).value = placeholder;
}
}, [])
...
console.log("after use effect:");
console.log(placeholder);
  return (
    <div style={error ? { border: "solid red" } : {}}>
...
    </div>
  );
};
export default AddressInput;
I would like to execute some code after the component has rendered. Normally I could use componentDidMount, but I'm not sure how to apply that when the component is constructed like above.
Since you're using a pure functional component you can use the useEffect hook (as of React v16.8) like this:
import React, { useEffect } from "react";
const AddressInput = (props) => {
const classes = useStyles();
const { disabled, error, location, onClear, placeholder, setLocation, showMap, value } = props;
useEffect(() => {
// your code here
}, []) // empty array acts like `componentDidMount()` in a functional component
return (
<div style={error ? { border: "solid red" } : {}}>
...
</div>
);
};
export default AddressInput;
You can read more about useEffect here:
https://reactjs.org/docs/hooks-effect.html

Rendering loop problem in ReactJS UseEffect Method

Hi I am new developer at ReactJs. I have a problem about useEffect rendering. I have an example and i am trying to change background image with time but useEffect make rendering so much and my background image is staying in a loop with time. I just want to change my images with order like bg1 bg2 bg3 etc.
How can I solve this infinite render?
my example .tsx
import React, { useEffect, useState } from 'react';
import { connect } from "../../../store/store";
const LeftPart = (props: any) => {
const [value, setValue] = useState(1);
const {loginLeftImagesLength} = props;
const changeLeftBackgroungImage : any = () =>{
const interval = setInterval(() => {
if (value <= loginLeftImagesLength.payload) {
setValue(value+1);
} else{
setValue(1);
}
}, 3000);
return () => clearInterval(interval);
};
useEffect(() => {
changeLeftBackgroungImage();
});
return (
<div className="col-xl-7 col-lg-7 col-md-7 col-sm col-12">
<img id="image" src={"../../../assets/images/bg"+value+".jpg"} style={{ width: "100%", height: "99vh" }} alt="Login Images"></img>
</div >
)
}
export default connect((store: any) => ({ loginLeftImagesLength: store.loginLeftImagesLength }))(LeftPart) as any;
You have infinite render as you have not specified any dependencies in your useEffect
useEffect(() => {
changeLeftBackgroungImage();
},[]); // Will run this hook on component mount.
Also you could do it in this way
useEffect(()=>{
const timer = setTimeout(()=>{
if (value <= loginLeftImagesLength.payload) {
setValue(value+1);
} else{
setValue(1);
}
},3000)
return ()=>{ // Return this function from hook which is called before the hook runs next time
clearTimeout(timer)
}
},[,value]) // RUN THIS HOOK ON componendDidMount and whenever `value` changes
Why not put this entire code inside the useEffect hook.
useEffect(() => ) {
const {loginLeftImagesLength} = props;
const changeLeftBackgroungImage : any = () =>{
const interval = setInterval(() => {
if (value <= loginLeftImagesLength.payload) {
setValue(value+1);
} else{
setValue(1);
}
}, 3000);
return () => clearInterval(interval);
};[changeBGStateHere])
use if else statements to change the background...
if (value === 1) {
changeLeftBackgroungImage();
} else (value === 2) {
and so on.
Let the interval change the state and let useEffect rerender when the state for the time changes.

Gatsby - IntersectionObserver is not defined

I am trying to build my gatsby project but I am unable due to the IntersectionObserver not being recognized. I use the intersectionObserver inside an InView component:
import React, { useRef, useState, useEffect } from 'react'
const InView = ({ children }) => {
const [boundingClientY, setBoundingClientY] = useState(null)
const [direction, setDirection] = useState(null)
const [element, setElement] = useState(null)
const [inView, setInView] = useState(false)
const observer = useRef(new IntersectionObserver((entries) => {
const first = entries[0]
const { boundingClientRect } = first
first.isIntersecting && setInView(true)
!first.isIntersecting && setInView(false)
boundingClientRect.y > boundingClientY && setDirection('down')
boundingClientRect.y < boundingClientY && setDirection('up')
boundingClientY && setBoundingClientY(first.boundingClientRect.y)
}))
useEffect(() => {
const currentElement = element
const currentObserver = observer.current
currentElement && currentObserver.observe(currentElement)
// console.log(currentObserver)
return () => {
currentElement && currentObserver.unobserve(currentElement)
};
}, [element])
const styles = {
opacity: inView ? 1 : 0,
transform: `
translateY(${!inView ?
direction === 'up' ? '-20px' : '20px'
: 0})
rotateY(${!inView ? '35deg' : 0})
scale(${inView ? 1 : 0.9})
`,
transition: 'all 0.4s ease-out 0.2s'
}
return (
<div ref={setElement} style={styles}>
{children}
</div>
)
}
export default InView
I have a wrapper for the root element to enable a global state and have tried importing the polyfill inside gatsby-browser.js:
import React from 'react'
import GlobalContextProvider from './src/components/context/globalContextProvider'
export const wrapRootElement = ({ element }) => {
return (
<GlobalContextProvider>
{element}
</GlobalContextProvider>
)
}
export const onClientEntry = async () => {
if (typeof IntersectionObserver === `undefined`) {
await import(`intersection-observer`);
}
}
This is an error on build, right ($ gatsby build)? If that's the case this has nothing to do with browser support.
It is the fact that IntersectionObserver is a browser API and you should not use browser APIs during server side rendering. Instead you try to utilize them after components have mounted. To solve this initialize your observer in useEffect() instead of useRef() as you currently do.
...
const observer = useRef();
useEffect(() => {
observer.current = new IntersectionObserver({ ... });
}, []); // do this only once, on mount
...
I got my jest test to pass by placing this before the creation of "new IntersectionObserver"
if (!window.IntersectionObserver) return
declare a let variable = null. this also works in NextJS
...
let observer = null
useEffect(()=> {
observer = new IntersectionObserver(callback,optional);
},[])
IntersectionObserver API is a browser API and it can't be executed during Gatbsy build process. Therefore, you should check if your code is running in browser:
let observer = null;
if (typeof window !== "undefined"){ // The code inside brackets will be executed ONLY in browser
observer = new IntersectionObserver(/* ... */);
// ...
}

React rewriting class with hooks causes re-render loops

I am working on an image transition react component, where it waits to have img1 loaded and then on a user click loads the img2, but fades img1 smoothly.
Tried re-writing the application with hooks to set the states - but when this is applied it creates an re-render loop error.
Is it because we are always setting img1 as currentImgSrc initially?
const [imgSrcOne, setImgSrcOne] = useState(currentImgSrc)
errors in the if/else block? or is setting the useState in the return causing the bug
tried removing the if/else block to make the application functional
https://jsfiddle.net/b531L6ho/
import {
ImageTransition
} from './imageTransition/imageTranition'
const {
useState
} = React
interface ImageContainerProps {
layer: string
currentImgSrc: string
notifyOnError: (url: string) => void
updateLayerLoading: (hasLoaded: boolean) = void
}
export const ImageTransitionContainer: React.SFC < ImageContainerProps > = ({
currentImgSrc,
layer,
notifyOnError,
updateLayerLoading
}) => {
const [imgSrcOne, setImgSrcOne] = useState(currentImgSrc)
const [displayImgOne, setdisplayImgOne] = useState(true)
const [imgOneLoaded, setImgOneLoaded] = useState(false)
const [imgSrcTwo, setImgSrcTwo] = useState(currentImgSrc)
const [displayImgTwo, setdisplayImgTwo] = useState(true)
const [imgTwoLoaded, setImgTwoLoaded] = useState(false)
if (imgSrcOne && currentImgSrc !== imgSrcOne) {
console.log("in the if statement")
setImgSrcTwo(currentImgSrc)
setDisplayImgTwo(two)
}
if (currentImgSrc !== imgSrcOne) {
setImgSrcne(currentImgSrc)
}
if (!imgSrcOne && !imgSrcTwo) {
setImgSrcOne(currentImgSrc)
setDisplayImgOne(true)
} else if (imgSrcOne && currentImgSrc !== imgSrcOne) {
setImgSrcTwo(currentImgSrc)
setDisplayImgTwo(true)
} else if (imgSrcTwo && currentImgSrc !== imgSrcTwo) {
setImgSrcOne(currentImgSrc)
setDisplayImgOne(true)
}
console.log("state --", imgSrcOne, displayImgOne, imgOneLoaded, imgSrcTwo, displayImgTwo, imgTwoLoaded)
return (
<>
<ImageTransition
displayImg={displayImgOne}
imgLoaded={imgOneLoaded}
imgSrc={imgSrcOne}
onExit={() => {
setImgSrcOne(null)
setImgOneLoaded(false)
}}
onLoad={() => {
setImgOneLoaded(true)
setDisplayImgTwo(false)
}}
/>
<ImageTransition
displayImg={displayImgTwo}
imgLoaded={imgTwoLoaded}
imgSrc={imgSrcTwo}
onExit={() => {
setImgSrcTwo(null)
setImgTwoLoaded(false)
}}
onLoad={() => {
setImgTwoLoaded(true)
setDisplayImgOne(false)
}}
/>
</>
)
}

Categories

Resources