Reactivity when element added to DOM - javascript

I want to integrate qr-scanner to my project. This library is capable of decoding QR-Codes by utilizing the camera. It just needs the reference to a html-video-element and then renders the webcam-stream and optionally some indicators of a QR-code being found to this element.
The easiest component would be something like this:
import { useRef } from "react";
import QrScanner from "qr-scanner";
export const QrComponent = () => {
const videoElement = useRef(null);
const scanner = new QrScanner(videoElement.current, (result) => {
console.log(result)
})
return (
<video ref={videoElement} />
)
}
however, qr-scanner checks, if the passed target-element is already part of the DOM:
if (!document.body.contains(video)) {
document.body.appendChild(video);
shouldHideVideo = true;
}
the video-element will never be added to the DOM, when the QrScanner-object is created. This leads to shouldHideVideo being set to true, which disables the video altogether later in the library-code.
So I think I need some kind of way to react to the video-element being added to the DOM. I thougt about using a MutationObserver (and tried it out by stealing the hook from this page), however I only wanted to print out all mutations using the hook like this:
import { useRef, useCallback } from "react";
import QrScanner from "qr-scanner";
import { useMutationObservable } from "./useMutationObservable";
export const QrComponent = () => {
const videoElement = useRef(null);
const scanner = new QrScanner(videoElement.current, (result) => {
console.log(result)
})
const onMutation = useCallback((mutations) => console.log(mutations), [])
useMutationObservable(document, onMutation)
return (
<video ref={videoElement} />
)
}
however, I never got a single line printed, so to me it seems, as if there are no mutations there.
Did I maybe misunderstand something? How can I react to the video-element being added to the document?

Ok, so I think I didn't provide enough information, but I found it out on my own:
The MutationObserver doesn't work, because I told it to watch document, yet I'm actually inside of a shadowdom with this particular component! So I suspect that mutations to the shadowdom won't be detected.
Also, because I'm inside of a shadowdom, document.contains(videoElem.current) will never be true. So in this particular case I have no better choice, than to copy the code of qr-scanner into my project-tree and adapt as needed.
On another note: useLayoutEffect is a react-hook, that is scheduled to run after DOM-mutations. So that's what I would use if I had to start over with this idea.

Related

React fast global redux-like variable

I'm making a front-end application using react/webgl and I need to be vary of performance improvements since almost everything must be rendered real-time and dynamically.
I need to render something on a canvas and need to use some variable's globally across many different components, but they need to be updated fast. Technically redux is what I need, however accessing dispatched variables takes time and causes crucial performance issues.
So instead I opted in to use useRef() which solves the “slow” issue but now I cannot update it’s value across different components. Using useRef() solves my issue but since it's not globally accessible it causes problems on other parts of the application.
Declaration of the variable looks like this:
import { useRef } from 'react';
const WebGLStarter = (props) => {
...
const myValue = useRef();
myValue.current = someCalculation();
function render(myValue.current){
...
requestAnimationFrame(function () {
render(myValue.current);
});
}
...
}
Currently someCalculation() is on the same component as it's declaration. I want to use someCalculation() on a different file but I can't do it beacuse useRef() won't allow me to. And again, I can't use redux because it's slow.
TL;DR : I need something similar to redux but it needs to be fast enough to not cause performance issues on an infinite loop.
Create a context with the ref. Wrap your root with the provider, and use the hook to get access to the ref when you need to use/update it:
import { createContext, useRef, useContext } from 'react';
const defaultValue = /** default value **/;
const MyValueContext = createContext(defaultValue);
const MyValueContextProvider = ({ children }) => {
const myValueRef = useRef(defaultValue);
return (
<MyValueContext.Provider value={myValueRef}>
{children}
</MyValueContext.Provider>
);
};
const useMyValue = () => useContext(MyValueContext);
To use in components call the useMyValue hook. This would give you direct access to the ref, and since you don't update any state (just change the ref's current property) it won't cause re-renders:
const WebGLStarter = (props) => {
const myValue = useMyValue();
myValue.current = someCalculation();
...
};

For some reason I am getting 4 responses when I should be getting one using axios? [duplicate]

I have a counter and a console.log() in an useEffect to log every change in my state, but the useEffect is getting called two times on mount. I am using React 18. Here is a CodeSandbox of my project and the code below:
import { useState, useEffect } from "react";
const Counter = () => {
const [count, setCount] = useState(5);
useEffect(() => {
console.log("rendered", count);
}, [count]);
return (
<div>
<h1> Counter </h1>
<div> {count} </div>
<button onClick={() => setCount(count + 1)}> click to increase </button>
</div>
);
};
export default Counter;
useEffect being called twice on mount is normal since React 18 when you are in development with StrictMode. Here is an overview of what they say in the documentation:
In the future, we’d like to add a feature that allows React to add and remove sections of the UI while preserving state. For example, when a user tabs away from a screen and back, React should be able to immediately show the previous screen. To do this, React will support remounting trees using the same component state used before unmounting.
This feature will give React better performance out-of-the-box, but requires components to be resilient to effects being mounted and destroyed multiple times. Most effects will work without any changes, but some effects do not properly clean up subscriptions in the destroy callback, or implicitly assume they are only mounted or destroyed once.
To help surface these issues, React 18 introduces a new development-only check to Strict Mode. This new check will automatically unmount and remount every component, whenever a component mounts for the first time, restoring the previous state on the second mount.
This only applies to development mode, production behavior is unchanged.
It seems weird, but in the end, it's so we write better React code, bug-free, aligned with current guidelines, and compatible with future versions, by caching HTTP requests, and using the cleanup function whenever having two calls is an issue. Here is an example:
/* Having a setInterval inside an useEffect: */
import { useEffect, useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000);
/*
Make sure I clear the interval when the component is unmounted,
otherwise, I get weird behavior with StrictMode,
helps prevent memory leak issues.
*/
return () => clearInterval(id);
}, []);
return <div>{count}</div>;
};
export default Counter;
In this very detailed article called Synchronizing with Effects, React team explains useEffect as never before and says about an example:
This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From the user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, and then pressing Back. React verifies that your components don’t break this principle by remounting them once in development.
For your specific use case, you can leave it as it's without any concern. And you shouldn't try to use those technics with useRef and if statements in useEffect to make it fire once, or remove StrictMode, because as you can read on the documentation:
React intentionally remounts your components in development to help you find bugs. The right question isn’t “how to run an Effect once”, but “how to fix my Effect so that it works after remounting”.
Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and a setup → cleanup → setup sequence (as you’d see in development).
/* As a second example, an API call inside an useEffect with fetch: */
useEffect(() => {
const abortController = new AbortController();
const fetchUser = async () => {
try {
const res = await fetch("/api/user/", {
signal: abortController.signal,
});
const data = await res.json();
} catch (error) {
if (error.name !== "AbortError") {
/* Logic for non-aborted error handling goes here. */
}
}
};
fetchUser();
/*
Abort the request as it isn't needed anymore, the component being
unmounted. It helps avoid, among other things, the well-known "can't
perform a React state update on an unmounted component" warning.
*/
return () => abortController.abort();
}, []);
You can’t “undo” a network request that already happened, but your cleanup function should ensure that the fetch that’s not relevant anymore does not keep affecting your application.
In development, you will see two fetches in the Network tab. There is nothing wrong with that. With the approach above, the first Effect will immediately get cleaned... So even though there is an extra request, it won’t affect the state thanks to the abort.
In production, there will only be one request. If the second request in development is bothering you, the best approach is to use a solution that deduplicates requests and caches their responses between components:
function TodoList() {
const todos = useSomeDataFetchingLibraryWithCache(`/api/user/${userId}/todos`);
// ...
Update: Looking back at this post, slightly wiser, please do not do this.
Use a ref or make a custom hook without one.
import type { DependencyList, EffectCallback } from 'react';
import { useEffect } from 'react';
const useClassicEffect = import.meta.env.PROD
? useEffect
: (effect: EffectCallback, deps?: DependencyList) => {
useEffect(() => {
let subscribed = true;
let unsub: void | (() => void);
queueMicrotask(() => {
if (subscribed) {
unsub = effect();
}
});
return () => {
subscribed = false;
unsub?.();
};
}, deps);
};
export default useClassicEffect;

Context value not accessible inside inner function of functional component

I am attempting to build a component that takes in an arbitrarily large list of items and displays a chunk of them at a time. As the user scrolls the window down, I want to automatically load more items if any exist.
The problem I am running into is that the my appState variable is not acting consistently. When I log it at the top of the component, it always reads the correct value out of the loaded context. However, when I read the value inside the onScroll function, it always returns the default uninitialized state. Where did my context go on the inner function?
Here's a stripped down version that illustrates my problem:
Component
import { useContext } from 'react'
import { useLifecycles} from 'react-use'
import AppState from '../../models/AppState'
import { Context } from '../../store/create'
export default () => {
const appState:AppState = useContext(Context)
console.log('appState.items (root)=', appState.items.length) // Returns `100`, as it should
useLifecycles(
() => {
window.addEventListener('scroll', onScroll)
},
() => {
window.removeEventListener('scroll', onScroll)
}
)
const onScroll = (evt:any) => {
console.log('appState.items (onScroll)', appState.items.length) // Returns `0` (the default uninitialized state).
}
return (
<div className='ItemList'>
<h1>Hello world</h1>
{/* The list of items goes here */}
</div>
)
}
../../store/create
import React from 'react'
import AppState, { getDefaultState } from '../models/AppState'
let state:AppState = getDefaultState()
export const Context:React.Context<AppState> = React.createContext<AppState>(state)
export const setAppState = (newState:AppState):void => {
_state = newState
}
export const getAppState = ():AppState => {
return _state
}
I've read the rule of hooks, and to my understanding I am not breaking anything. My useContext and useLifecycle calls are in a fixed order at the top; no conditionals, no loops.
What am I missing?
I am not aware of how useLifecycles work. But the problem I can see is that you are binding the event a function. That function has the state in it's closure and so it captures that value of state. Whenever state changes, your handler isn't aware of the state change and so it just keeps using the data that was previously captured. Now to solve it, you need to listen for state change and remove the listener that was previously attached, add the new listener that has new values in its closure. I think the useLifecycles should have a dependency option to achieve that. If not the other way could be to use useEffect hook.
Edit:
I just checked the react-use docs and turns out what you really need is useEvent. Look at the example in docs. To make sure it works in your case, you should pass your dependency in useCallback.

Why useEffect running twice and how to handle it well in React?

I have a counter and a console.log() in an useEffect to log every change in my state, but the useEffect is getting called two times on mount. I am using React 18. Here is a CodeSandbox of my project and the code below:
import { useState, useEffect } from "react";
const Counter = () => {
const [count, setCount] = useState(5);
useEffect(() => {
console.log("rendered", count);
}, [count]);
return (
<div>
<h1> Counter </h1>
<div> {count} </div>
<button onClick={() => setCount(count + 1)}> click to increase </button>
</div>
);
};
export default Counter;
useEffect being called twice on mount is normal since React 18 when you are in development with StrictMode. Here is an overview of what they say in the documentation:
In the future, we’d like to add a feature that allows React to add and remove sections of the UI while preserving state. For example, when a user tabs away from a screen and back, React should be able to immediately show the previous screen. To do this, React will support remounting trees using the same component state used before unmounting.
This feature will give React better performance out-of-the-box, but requires components to be resilient to effects being mounted and destroyed multiple times. Most effects will work without any changes, but some effects do not properly clean up subscriptions in the destroy callback, or implicitly assume they are only mounted or destroyed once.
To help surface these issues, React 18 introduces a new development-only check to Strict Mode. This new check will automatically unmount and remount every component, whenever a component mounts for the first time, restoring the previous state on the second mount.
This only applies to development mode, production behavior is unchanged.
It seems weird, but in the end, it's so we write better React code, bug-free, aligned with current guidelines, and compatible with future versions, by caching HTTP requests, and using the cleanup function whenever having two calls is an issue. Here is an example:
/* Having a setInterval inside an useEffect: */
import { useEffect, useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000);
/*
Make sure I clear the interval when the component is unmounted,
otherwise, I get weird behavior with StrictMode,
helps prevent memory leak issues.
*/
return () => clearInterval(id);
}, []);
return <div>{count}</div>;
};
export default Counter;
In this very detailed article called Synchronizing with Effects, React team explains useEffect as never before and says about an example:
This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From the user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, and then pressing Back. React verifies that your components don’t break this principle by remounting them once in development.
For your specific use case, you can leave it as it's without any concern. And you shouldn't try to use those technics with useRef and if statements in useEffect to make it fire once, or remove StrictMode, because as you can read on the documentation:
React intentionally remounts your components in development to help you find bugs. The right question isn’t “how to run an Effect once”, but “how to fix my Effect so that it works after remounting”.
Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and a setup → cleanup → setup sequence (as you’d see in development).
/* As a second example, an API call inside an useEffect with fetch: */
useEffect(() => {
const abortController = new AbortController();
const fetchUser = async () => {
try {
const res = await fetch("/api/user/", {
signal: abortController.signal,
});
const data = await res.json();
} catch (error) {
if (error.name !== "AbortError") {
/* Logic for non-aborted error handling goes here. */
}
}
};
fetchUser();
/*
Abort the request as it isn't needed anymore, the component being
unmounted. It helps avoid, among other things, the well-known "can't
perform a React state update on an unmounted component" warning.
*/
return () => abortController.abort();
}, []);
You can’t “undo” a network request that already happened, but your cleanup function should ensure that the fetch that’s not relevant anymore does not keep affecting your application.
In development, you will see two fetches in the Network tab. There is nothing wrong with that. With the approach above, the first Effect will immediately get cleaned... So even though there is an extra request, it won’t affect the state thanks to the abort.
In production, there will only be one request. If the second request in development is bothering you, the best approach is to use a solution that deduplicates requests and caches their responses between components:
function TodoList() {
const todos = useSomeDataFetchingLibraryWithCache(`/api/user/${userId}/todos`);
// ...
Update: Looking back at this post, slightly wiser, please do not do this.
Use a ref or make a custom hook without one.
import type { DependencyList, EffectCallback } from 'react';
import { useEffect } from 'react';
const useClassicEffect = import.meta.env.PROD
? useEffect
: (effect: EffectCallback, deps?: DependencyList) => {
useEffect(() => {
let subscribed = true;
let unsub: void | (() => void);
queueMicrotask(() => {
if (subscribed) {
unsub = effect();
}
});
return () => {
subscribed = false;
unsub?.();
};
}, deps);
};
export default useClassicEffect;

Stop Audio on Route Change in React?

everyone. I'm still getting used to React and React Router, and this is one thing I have no figured out.
So, I have an app that plays a video (muted) and an audio track at the same time. I am using React Player to play both. My view looks like this (VideoPlayer is the react-player tag):
<div>
<VideoPlayer url={this.props.audio} playing={this.state.playing} />
<VideoPlayer url={this.props.video} playing={this.state.playing} />
</div>
This setup has worked for me, and I am able to play and control them both via a common state. I can stop them via an event handler I hooked up to a button:
handleStop() {
this.setState({playing: false})
}
And this works as well.
The issue, however, is that once I navigate to a different route, the audio (and presumably the video) remains playing in the background. Actually, let me rephrase, the audio restarts in the background when I change routes.
After reading this page from the react-router docs, I have tried to include logic to call handleStop in various lifecycle events, yet none of them does the trick. So far, I have tried putting calls to handleStop in componentWillReceiveProps, componentWillUpdate, componentDidUpdate and componentWillUnmount.
The closest I got was putting the call in componentWillUnmount, but I always receive an error about setting the state of an unmounted component (which doesn't make sense either, if this is called before unmounting?).
So, by any chance, does anybody know where to put my call to handleStop?
Thanks in advance.
I know this question is over 4 years old, but I had this problem today and wanted to share my way of solving it...
import React, { useRef, useEffect } from 'react';
import waves from '../audio/waves.mp3';
const RockyCoast = (props) => {
// the audio variable needs to be stored in a ref in order to access it across renders
let audio = useRef();
// start the audio (using the .current property of the ref we just created) when the component mounts using the useEffect hook
useEffect(() => {
audio.current = new Audio(waves)
audio.current.play()
}, [])
// Stop the audio when the component unmounts
// (not exactly what you asked re React Router, but similar idea)
useEffect(() => {
return () => {
audio.current.pause()
console.log("in cleanup")
}
}, [])
...
return (
<>
...
</>
)
}
export default RockyCoast;
Some of the key reasons this works properly is because we're declaring the audio variable as a React ref, so that we can access it again to pause it on unmount. It's also important that the dependency arrays for the two useEffect calls are the same (in my case they were empty, in order for them to act like componentDidMount and componentWillUnmount).
The question was asking specifically about changing routes using React Router, so there may be a little more work required for your specific circumstance, but if like me you have one parent component being rendered for the route where we need this feature, like this:
<Route exact path="/habitat/rocky_coast">
<RockyCoast />
</Route>
(audio playing on the page, then stopping when we navigate to a different page), this solution works beautifully.
Hopefully this helps some other poor dev, who like me managed to create a project with a ton of audio clips playing over themselves lol!
In my own audio player I had created using useState when the user was in some exact views the audio continue to play, so I have solved this problem using recoil Atoms & useRecoilState and with react-router useLocation & useNavigate
AudioAtom.js:
import { atom } from "recoil";
const playingAudioState = atom({
key: "playingAudioState",
default: false,
});
export default playingAudioState;
Player.jsx:
import { useState, useEffect } from "react";
import { useRecoilState } from "recoil";
import playingAudioState from "./AudioAtom";
function Player() {
const [audio, setAudio] = useState(new Audio(url));
const [playing, setPlaying] = useRecoilState(playingAudioState);
useEffect(() => {
playing ? audio.play() : audio.pause();
}, [playing]);
...
return <>
...
</>
}
CustomRoutes.jsx:
import { useEffect} from "react";
import { useRecoilState } from "recoil";
import playingAudioState from "./AudioAtom";
import { ... , useLocation, useNavigate } from "react-router-dom";
function CustomRoutes() {
const navigate = useNavigate();
const location = useLocation();
const [playing, setPlaying] = useRecoilState(playingAudioState);
const routesWithoutAudio = ["/", "/example", "/hello"];
useEffect(() => {
if (playing && routesWithoutAudio.includes(location.pathname)) {
// if the audio is playing and the user is in a route with no audio we will set
// the state to false and refresh / reload de current page to not listen to it
setPlaying(false);
navigate(0 , { replace: true });
}
}, [location]);
return (
...Routes
)
I never found an answer, even after moving the logic to push the URL into handleStop. So I just changed page using window.location.href = '...'. Hacky, but it worked.

Categories

Resources