reactjs run code after all elements are loaded - reactjs

I am in my first steps with react. I am running Reactjs v16.11.0
I have page which trigger the webcam (following this mdn tutorial). So I want to call startup function when all elements were painted. I tried with window.addEventListener('load', startup, false); but it doesn't call any function.
So I tried the useEffectHook:
useEffect (() => {
startup();
}, []);
But it call the startup function too soon, and there is some elments that there aren't still in the DOM because it runs asyncronous code - video .
My startup function is this
const startup= () => {
video = document.getElementById('video');
let canvas: HTMLCanvasElement = document.getElementById('canvas') as HTMLCanvasElement;
const photo = document.getElementById('photo') as HTMLElement;
navigator.mediaDevices.getUserMedia({video: true, audio: false})
.then(function(stream) {
mediaStream = stream.getTracks()[0];
video.srcObject = stream;
video.play();
})
.catch(function(err) {
console.log("An error occurred: " + err);
});
if(video) {
video.addEventListener('canplay', function (ev: any) {
if (!streaming) {
height = video.videoHeight / (video.videoWidth / width);
// Firefox currently has a bug where the height can't be read from
// the video, so we will make assumptions if this happens.
if (isNaN(height)) {
height = width / (4 / 3);
}
video.setAttribute('width', width);
video.setAttribute('height', height);
canvas.setAttribute('width', width.toString());
canvas.setAttribute('height', height.toString());
streaming = true;
}
}, false);
//clearphoto(canvas, photo);
}
}
I am using functional component (instead of class component). And from what I understood componentDidMount works with class component. Am I correct?
How can accomplish to run the startup function only when every elements are in the DOM ?
EDIT: code edit in useEffect hook, noticed by Jayraj

I have just finished following the tutorial. It was interesting to me, as well.
First of all, you can play my demo: https://codesandbox.io/s/hungry-bassi-ojccj?file=/src/App.js.
To achieve the goal I used three hooks: useRef, useEffect, useState. Let's me explain why I have used each of them.
So, I would like to start with the useState hook. Before streaming, we should calculate the height of the image and canvas and set it. However, we must save the height into somewhere to get its value in our component. That's why I used the useState hook.
To draw canvas successfully I used the useRef hook. It allows me to access the DOM and that's why I removed calls the getElementById from the code as the hook is responsible for. I made the same with the video. I created the videoRef to access the DOM.
And the main that I called the useEffect hook two times. As you can see, the first useEffect hasn't any dependencies that's why it will be called once. It works like the componentDidMount method in this case. Thankfully to it, the getUserMedia method is called and we can set stream to the videoRef and afterwards the video will be started playing.
The second useEffect waitings for the changes of the videoRef property and then it starts executing.
I guess you should read about react hook more deeply to understand very well. Let's me attach the link of the documentation. https://reactjs.org/docs/hooks-reference.html
Have a good day.

Related

IntersectionObserver Flickering with ScrollIntoView

I'm trying to build a custom input that you can change its value by scrolling with IntersectionObserver and ScrollIntoView
The problem that I'm facing is that when I try to make the component controlled with a state it starts to flicker when scrolling.
I have the example here in this sandbox, and you can see the input gets initialized correctly with the correct value, but when you try to change it.. there is a flickering at the beginning of the scroll event. also resetting the input by the button does seem to work correctly.
I'm not really able to figure out how to get the updates correctly done in each event since I'm very new to Intersection observer
Try setting the threshold value to 1 such that it will fire only when it goes out of boundary completely.
const observer = new IntersectionObserver(
(entries) => {
const selectedEntry = entries.find(
(e) => Number.parseFloat(e.target.textContent) === value
);
selectedEntry?.target?.scrollIntoView();
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
!isFirstRender &&
onChange(Number.parseFloat(entry.target.textContent));
});
},
{ threshold: 1 } // changed to 1
);
Also please do as the linter says, and add proper dependencies for the useEffect hook unless when not needed.
If you are using React, you might consider react-intersection-observer.
In my case, I was able to remove flickering by setting option triggerOnce: true.

React Testing Library - Failing to test Window Resize React Hook

I've created a custom React Hook for getting the viewport width & height on window resize (the event is debounced). The hook works fine, but I've been unable to find a way to test with React Testing Library (I keep running into errors).
I've recreated the app in CodeSandbox (along with the tests) to try to debug, but I'm running into different errors while testing.
Sometimes I get:
Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'.`
But the general failures are that the data from the hook does not seem to be returned.
expect(received).toBe(expected) // Object.is equality
Expected: 500
Received: undefined
This could be something I'm missing with React Testing Library.
Any help getting to the bottom of the problem would be super appreciated!
Demo app/tests here:
https://codesandbox.io/s/useviewportsize-4l7gb?file=/src/use-viewport-size.test.tsx
===========
Solution
Thanks to #tmhao2005 it seemed that the problem was down to the hook getting the resize values from document rather than window:
setViewportSize({
width: window.innerWidth, //document.documentElement.clientWidth - doesn't work
height: window.innerHeight //document.documentElement.clientHeight - doesn't work
});
It seems that getting the clientWidth/Height is fine in the app, but fails in the React Testing Library tests.
I was opting for client sizing as I believe that does not include scollbar widths.
I think there are a few things you have to change to make your test working again:
You haven't waited to your debounce function work which is the main problem. So you can use either mock the timer or wait until your debounce function getting called.
// Make your test as `async` in case of wanting to wait
test("should return new values on window resize", async () => {
// If you go for mocking timer, uncomment this & below advance the timer
// jest.useFakeTimers();
const { result } = renderHook(() => useViewportSize());
act(() => {
window.resizeTo(500, 500);
//fireEvent(window, new Event("resize"));
});
// jest.advanceTimersByTime(251) // you can also use this way
await mockDelay(debounceDelay); // `await` 300ms to make sure the function callback run
expect(result.current.width).toBe(500);
expect(result.current.height).toBe(500);
});
You might refine your implementation code by changing to use your mock value instead:
const debouncedHandleResize = debounce(() => {
setViewportSize({
// using your mock values
width: window.innerWidth,
height: window.innerHeight
});
}, debounceTime);
PS: I also edited your codesandbox based on the async way: https://codesandbox.io/s/useviewportsize-forked-pvnc1?file=/src/use-viewport-size.test.tsx

React Hooks API call - does it have to be inside useEffect?

I'm learning React (with hooks) and wanted to ask if every single API call we make has to be inside the useEffect hook?
In my test app I have a working pattern that goes like this: I set the state, then after a button click I run a function that sends a get request to my API and in the .then block appends the received data to the state.
I also have a useEffect hook that runs only when the said state changes (using a dependency array with the state value) and it sets ANOTHER piece of state using the new data in the previous state. That second piece of state is what my app renders in the render block.
This way my data fetching actually takes place in a function run on a button click and not in the useEffect itself. It seems to be working.
Is this a valid pattern? Thanks in advance!
Edit: example, this is the function run on the click of the button
const addClock = timezone => {
let duplicate = false;
selectedTimezones.forEach(item => {
if (item.timezone === timezone) {
alert("Timezone already selected");
duplicate = true;
return;
}
});
if (duplicate) {
return;
}
let currentURL = `http://worldtimeapi.org/api/timezone/${timezone}`;
fetch(currentURL)
.then(blob=>blob.json())
.then(data => {
setSelectedTimezones(prevState => [...prevState, data]);
}
);
}
Yes, apis calls that happen on an action like button click will not be part of useEffect call. It will be part of your event handler function.
When you call useEffect, you’re telling React to run your “effect”
function after flushing changes to the DOM
useEffect contains logic which we would like to run after React has updated the DOM. So, by default useEffect runs both after the first render and after every update.
Note: You should always write async logic inside useEffect if it is not invoked by an event handler function.
Yes, you can make api requests in an event handler such as onClick.
What you don't want to do is make a request directly inside your functional component (since it will run on every render). As long as the request is inside another function and you only call that function when you actually want to make a request, there is no problem.

Spooky Action at a Distance: Electron & React useEffect - Unable to unsubscribe from ipcRenderer events

I have encountered strange behavior when using Electron's ipcRenderer with React's useEffect.
Within my electron app, I have the following code:
import React, { useEffect } from 'react'
const electron = window.require('electron');
const ipcRenderer = electron.ipcRenderer;
...
const someValueThatChanges = props.someValue;
useEffect(() => {
const myEventName = 'some-event-name';
console.log(`Using effect. There are currently ${ipcRenderer.listenerCount(eventName)} listeners.`);
console.log(`Value that has changed: ${someValueThatChanges}.`);
ipcRenderer.addListener(myEventName, myEventHandler);
console.log('Added a new listener.');
// Should clean up the effect (remove the listener) when the effect is called again.
return () => {
ipcRenderer.removeListener(myEventName, myEventHandler)
console.log('Cleaned up event handler.');
}
}, [ someValueThatChanges ]);
function myEventHandler() {
console.log('Handled event');
}
The code above is supposed to listen to the some-event-name event fired by Electron's main process with mainWindow.webContents.send('some-event-name'); and console.log(...) a message inicating that the event was handled.
This works as expected when the effect is initially run. A listener is added, the event is raised at a later time, and the string 'Handled event' is printed to to the console. But when the someValueThatChanges variable is assigned a different value and the event is raised for a second time, the 'Handled event' string is printed out to the console twice (the old listener does not appear to have been removed).
The line with the listenerCount(eventName) call returns 0 as expected when the removeListener(...) call is included in the useEffect return/cleanup function. When the removeListener(...) call is removed, the listenerCount(eventName) call returns a value that is incremented as expected (e.g. 0, 1, 2) as listeners are not removed.
Here's the really weird part. In either case, whether or not I include the call to removeListener(...), the myEventHandler function is always called for as many times as useEffect has been run. In other words, Electron reports that there are no event listeners, but myEventHandler still seems to be called by the previous listeners. Is this a bug in Electron, or am I missing something?
Never try with ipcRenderer.addListener, But try ipcRenderer.on instead
useEffect(() => {
ipcRenderer.send('send-command', 'ping');
ipcRenderer.on('get-command', (event, data) => {
console.log('data', data);
});
return () => {
ipcRenderer.removeAllListeners('get-command');
};
}, []);
I believe, the docs changed. ipcRenderer.removeAllListeners accept single string instead of array of string Source electron issues,

Fit map to feature layer bounds in react-leaflet

What I want to achieve:
Have a <Map><FeatureGroup><Circle />[1 or more]...</FeatureGroup></Map> hierarchy and fit the map bounds to the feature group so that all the circles are in the viewport.
If there is only one circle, it should fit the bounds (ie: zoom in on) to that circle.
What I've tried:
giving FeatureGroup a ref and calling getBounds on it to pass onto Map. Because of the lifecycle FeatureGroup doesn't exist at the time componentDidMount is called - it gets rendered later (https://github.com/PaulLeCam/react-leaflet/issues/106#issuecomment-161594328).
Storing Circle in state and calling getBounds on that (assuming, in this case, that there is only one circle. That didn't work either.
I think I might need to do something with the React Context but I'm not sure that I fully understand it right now, so I need some help.
Other information
I'm using react-leaflet#2.1.2
Thanks for any help offered!
Because the contents of the Map are unavailable at componentDidMount-time (https://github.com/PaulLeCam/react-leaflet/issues/106#issuecomment-161594328) you cannot get the bounds of the FeatureGroup at that point, and out of all the refs you assign, only the Map ref will be available in this.refs.
However, as per this GitHub comment: https://github.com/PaulLeCam/react-leaflet/issues/106#issuecomment-366263225 you can give a FeatureGroup an onAdd handler function:
<FeatureGroup ref="features" onAdd={this.onFeatureGroupAdd}>...
and you can then use the Map refs to access the leafletElement and call fitBounds with the bounds of the incoming event target, which will be the FeatureGroup:
onFeatureGroupAdd = (e) => {
this.refs.map.leafletElement.fitBounds(e.target.getBounds());
}
This will then "zoom" the map into the bounds of your FeatureGroup, as desired.
Update
I modified my React component so that zoom and centre are controlled by query parameters. The problem with the above solution was that if you zoomed in on a MarkerClusterGroup by clicking on it, for example, it would update the zoom in the url, re-render the map and re-call onFeatureGroupAdd, thus undoing all the marker cluster goodness.
What I needed was to access the zoom level required to keep the newly drawn circle nicely in bounds, then update the url with the correct zoom level and center.
onDrawCircle = (e) => {
...
var targetZoom = this.refs.map.leafletElement.getBoundsZoom(e.layer.getBounds());
// Call function to update url here:
functionToUpdateUrl(targetZoom, e.layer.getBounds().getCenter());
}
}
In order to be able to control the whole map I also call functionToUpdateUrl in onZoomEnd and onDragEnd event handlers, like so:
onChangeView = (e) => {
functionToUpdateUrl(e.target._zoom, this.refs.map.leafletElement.getCenter());
}
and one for handling cluster clicks:
onClusterClick = (e) => {
// This time we want the center of the layer, not the map?
functionToUpdateUrl(e.target._zoom, (e.layer ? e.layer.getBounds().getCenter() : e.target.getBounds().getCenter()));
}
Then, when rendering the Map element, pass these properties:
<Map
center={center}
ref='map'
zoom={zoom}
maxZoom={18}
onZoomEnd={this.onChangeView}
onDragEnd={this.onChangeView}
>
....
</Map>
And remember to give any MarkerClusterGroups their onClusterClick callback:
<MarkerClusterGroup onAdd={this.onMarkerGroupAdd} onClusterClick={this.onClusterClick}>
Have you tried doing getBounds in the componentDidMount function instead of componentWillMount? If that doesn't work then I'd suggest extending the FeatureGroup component and adding an onLoaded function as as prop and call that function in the componentDidMount function of your extended component. And by extending the FeatureGroup component I actually mean copying/pasting it from here. (if you care about why you need to copy that whole file check this thread)
This isn't tested but your code will probably look something like
import { FeatureGroup } from 'leaflet';
import { withLeaflet, Path } from 'react-leaflet';
class CustomFeatureGroup extends Path {
createLeafletElement(prop) {
const el = new FeatureGroup(this.getOptions(props));
this.contextValue = {
...props.leaflet,
layerContainer: el,
popupContainer: el,
};
return el;
}
componentDidMount() {
super.componentDidMount();
this.setStyle(this.props);
/*
Here you can do your centering logic with an onLoad callback or just
by using this.leafletElement.map or whatever
*/
this.props.onLoaded();
}
}
export default withLeaflet(CustomFeatureGroup)
Note: If you are using react-leaflet V1 this is actually way easier and I can edit this answer with that code if needed.

Resources