ReactJs : useRef current getting null - reactjs

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

Related

Can't perform a React state update with useMediaQuery

I'm trying to use useMediaQuery with NextJS to conditionally render a background image, but i get
"Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. "
when getStaticProps starts.
I tried to add some cleanup function, but with no results.
export const getStaticProps: GetStaticProps = async () => {
const rocketData = await fetchPlanetsInfo("technology");
return {
props: {
data: rocketData,
},
};
};
export default function Technology({ data }: Props) {
const isMobile = useMediaQuery({ query: "(max-width: 30em)" });
const [mobileView, setMobileView] = React.useState(false);
React.useEffect(() => {
setMobileView(isMobile);
}, [isMobile]);
return (
<Layout>
<MainPagesComponents
backgroundImage={spaceLaunchBackground}
title={"03 SPACE LAUNCH 101"}
>
<Swiper
// direction={"vertical"}
modules={[Pagination, Autoplay]}
spaceBetween={0}
slidesPerView={"auto"}
centeredSlides={true}
scrollbar={{ draggable: true }}
autoplay={{
delay: 5000,
pauseOnMouseEnter: true,
disableOnInteraction: false,
}}
pagination={{
clickable: true,
bulletActiveClass: "tech-active-class",
bulletClass: "swiper-tech",
horizontalClass: "swiper-tech-position-container",
renderBullet: (index, className) => {
return `
<div class='${className}'>${index + 1}</div>
`;
},
}}
className="mySwiper"
>
{data.map((rocket) => {
const portraitImage = rocket.images.portrait.slice(1);
const landscapeImage = rocket.images.landscape.slice(1);
const viewImage = {
width: mobileView ? "375px" : "515px",
height: mobileView ? "170px" : "527px",
};
return (
<SwiperSlide key={uuidv4()}>
<TechnologySlider
view={viewImage}
image={mobileView ? landscapeImage : portraitImage}
title={rocket.name.toUpperCase()}
description={rocket.description}
/>
</SwiperSlide>
);
})}
</Swiper>
</MainPagesComponents>
</Layout>
);
UPDATE:
I create a simple gif where i'm recording the error,
https://gifyu.com/image/SHbRd
Is the error occurring when you call the getStaticProps function? If that's the case, you need to fix how you're returning the prop data. Maybe throw in a promise after the fetch occurs and then return the data if it's successful.
Also to clean up a function in react, the useEffect needs to return a callback function. I think just wrapping the useMediaQuery inside This callback function is run every time the component unmounts. Here's an example:
useEffect(() => {
//do something
...
//clean up
return () => {
//thing you want to clean up
}
}, [])
You can read more about it here: https://reactjs.org/docs/hooks-effect.html
I you need to check if the render is mounted to perform state update.
here's a custom hook that helps you check if the component is mounted, you can use it as condition to run states update and avoid the error
//our custom hook
export const useIsMounted = () => {
const ref = React.useRef(false)
const [, setIsMounted] = React.useState(false)
React.useEffect(() => {
ref.current = true
setIsMounted(true)
return () => (ref.current = false)
}, [])
return () => ref.current
}
then use import it or use it on same page file
...
const isMobile = useMediaQuery({ query: "(max-width: 30em)" });
const [mobileView, setMobileView] = React.useState(false);
const isMounted = useIsMounted()
React.useEffect(() => {
//update state only if the component is mounted
if(isMounted) setMobileView(isMobile);
}, [isMobile, isMounted ]); //add it as dependency here
...

React: window event subscription

I am getting into React and trying to create some kind of keyboard trainer app.
I want to add a keypress event listener to the window object to get the letters, and then update the state I created with useState() hook.
So I add the event listener inside useEffect hook. with the custom handler. But the setPhrase function doesn't seem to work well in this case.
Expected result:
After each correct key press the phrase.written to add this key as text, phrase.left to substring by this letter;
Actual result:
State is renewed every time, so the state doesn't update after setPhrase call
The code:
export default () => {
const [initialPhrase] = useState("Test Phrase");
const [phrase, setPhrase] = useState({
left: initialPhrase,
written: "",
});
const handleKeyPress = (event) => {
const requiredLetter = phrase.left.charAt(0);
if (requiredLetter === event.key) {
setPhrase({
written: phrase.written + event.key,
left: phrase.left.substring(1, phrase.left.length),
});
}
console.debug(phrase, requiredLetter, event.key, phrase);
};
useEffect(() => {
window.addEventListener("keypress", handleKeyPress);
return () => window.removeEventListener("keypress", handleKeyPress);
}, []);
return (
<div>
<p className="phrase">
<span className="phrase--part phrase--part__written">
{phrase.written}
</span>
<span className="phrase--part phrase--part__left">{phrase.left}</span>
</p>
</div>
);
};
Playground:
https://stackblitz.com/edit/react-qgbjaf?file=src/index.js
Note: if you place this handler to any element inside component, it is going to work just as expected
The problem is that the handleKeyPress Method is not updated after the phrase state updated. Therefore, you need to add the handleKeyPress method to the useEffect dependency. Now after each phrase state update, the method gets the correct state of phrases
Stackblitz fork: https://stackblitz.com/edit/react-bx5qp7?file=src/index.js
import React, { useState, useEffect, useCallback } from 'react';
import ReactDOM from 'react-dom';
import './style.css';
const KeyboardTrainer = () => {
const [initialPhrase] = useState('Test Phrase');
const [phrase, setPhrase] = useState({
left: initialPhrase,
written: '',
});
const handleKeyPress = (event) => {
const requiredLetter = phrase.left.charAt(0);
if (requiredLetter === event.key) {
setPhrase({
written: phrase.written + event.key,
left: phrase.left.substring(1, phrase.left.length),
});
}
}
useEffect(() => {
window.addEventListener('keypress', (e) => {
handleKeyPress(e);
});
return () => window.removeEventListener("keypress", handleKeyPress);
}, [handleKeyPress]);
return (
<div>
<p className="phrase">
<span className="phrase--part phrase--part__written">
{phrase.written}
</span>
<span className="phrase--part phrase--part__left">{phrase.left}
</span>
</p>
</div>
)};
ReactDOM.render(<KeyboardTrainer />, document.getElementById('root'));
in your event handler method you cant reach the updated state, because state variables don't update itself inside an EventListener. just use useRef like this:
const phraseRef = useRef({
left: initialPhrase,
written: '',
})
const handleKeyPress = (event) => {
const requiredLetter = phraseRef.current.left.charAt(0);
if (requiredLetter === event.key) {
setPhrase({
written: phraseRef.current.written + event.key,
left: phraseRef.current.left.substring(1, phraseRef.current.left.length),
});
phraseRef.current = {
written: phraseRef.current.written + event.key,
left: phraseRef.current.left.substring(1, phrase.left.length),
}
}
console.debug(phrase, requiredLetter, event.key, phrase);
};

InvalidStateError: Failed to execute 'stop' on 'MediaRecorder': The MediaRecorder's state is 'inactive'

const [rec, setRec] = useState({});
const [onRec, setOnRec] = useState(true);
useEffect(() => {
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
const mediaRecorder = new MediaRecorder(stream)
setRec(mediaRecorder)
})
}, [onRec]);
This is useEffect.
const onRecAudio = () => {
rec.start()
console.log(rec);
console.log("start")
setOnRec(false)
}
This is first click of function. recording start.
const offRecAudio = () => {
rec.stop()
console.log("stop")
setOnRec(true)
}
This is second click of function. recording end.
<button onClick={onRec ? onRecAudio : offRecAudio } />
I don't want the useEffect to run those statements when the component is first rendered, but just click a button to run them. Press once to start recording, press again to end recording. But when I press it again, I see this error.
I've come across the same error but with different patter, I didn't save MediaRecorder in state because I found it's hard to deal with complex objects and Web API interfaces when saving them and restore them from the state, so I used a react ref and two buttons for start and stop, the error was showing when I've forget to unbind the stop event listener, but after unbinding it, it works great.
I'll share my code with you and for the future people who want to record audio using react in a simple way:
import { useRef } from 'react';
declare var MediaRecorder: any;
export function ChatsInputs() {
const stopButtonRef = useRef<HTMLButtonElement>(null);
function startRecording() {
navigator
.mediaDevices
.getUserMedia({ audio: true, video: false })
.then(function (stream) {
const options = { mimeType: 'audio/webm' };
const recordedChunks: any = [];
const mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.addEventListener('dataavailable', function (e: any) {
if (e.data.size > 0) recordedChunks.push(e.data);
});
mediaRecorder.addEventListener('stop', function () {
setBlobUrl(URL.createObjectURL(new Blob(recordedChunks)));
});
if (stopButtonRef && stopButtonRef.current)
stopButtonRef?.current?.addEventListener('click', function onStopClick() {
mediaRecorder.stop();
this.removeEventListener('click', onStopClick)
});
mediaRecorder.start();
});
}
return (
<div>
<button onClick={startRecording}>{'rec'}</button>
<button ref={stopButtonRef}>{'stop'}</button>
<a download="file.wav" href={blobUrl}>{'download audio'}</a>
{
blobUrl ?
<audio id="player" src={blobUrl} controls></audio>
:
null
}
</div>
);
}
Ref Article

Scroll to element on page load with React Hooks

I'm trying to create a functional component that fetches data from an API and renders it to a list. After the data is fetched and rendered I want to check if the URL id and list item is equal, if they are then the list item should be scrolled into view.
Below is my code:
import React, { Fragment, useState, useEffect, useRef } from "react";
export default function ListComponent(props) {
const scrollTarget = useRef();
const [items, setItems] = useState([]);
const [scrollTargetItemId, setScrollTargetItemId] = useState("");
useEffect(() => {
const fetchData = async () => {
let response = await fetch("someurl").then((res) => res.json());
setItems(response);
};
fetchData();
if (props.targetId) {
setScrollTargetItemId(props.targetId)
}
if (scrollTarget.current) {
window.scrollTo(0, scrollTarget.current.offsetTop)
}
}, [props]);
let itemsToRender = [];
itemsToRender = reports.map((report) => {
return (
<li
key={report._id}
ref={item._id === scrollTargetItemId ? scrollTarget : null}
>
{item.payload}
</li>
);
});
return (
<Fragment>
<ul>{itemsToRender}</ul>
</Fragment>
);
}
My problem here is that scrollTarget.current is always undefined. Please advice what I'm doing wrong. Thanks in advance.
Using useCallback, as #yagiro suggested, did the trick!
My code ended up like this:
const scroll = useCallback(node => {
if (node !== null) {
window.scrollTo({
top: node.getBoundingClientRect().top,
behavior: "smooth"
})
}
}, []);
And then I just conditionally set the ref={scroll} on the node that you want to scroll to.
That is because when a reference is changed, it does not cause a re-render.
From React's docs: https://reactjs.org/docs/hooks-reference.html#useref
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
constructor(props) {
thi.modal = React.createRef();
}
handleSwitch() {
// debugger
this.setState({ errors: [] }, function () {
this.modal.current.openModal('signup') // it will call function of child component of Modal
});
// debugger
}
return(
<>
<button className="login-button" onClick={this.handleSwitch}>Log in with email</button>
<Modal ref={this.modal} />
</>
)
React Hooks will delay the scrolling until the page is ready:
useEffect(() => {
const element = document.getElementById('id')
if (element)
element.scrollIntoView({ behavior: 'smooth' })
}, [])
If the element is dynamic and based on a variable, add them to the Effect hook:
const [variable, setVariable] = useState()
const id = 'id'
useEffect(() => {
const element = document.getElementById(id)
if (element)
element.scrollIntoView({ behavior: 'smooth' })
}, [variable])

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

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)
})

Resources