Why does a pause reset after enabling full screen mode? - reactjs

I'm trying to add a video player, I'm using react-native-video-controls to add controls on my video but it has a problem with resetting a pause
code:
handleExitFullScreen = () => {
this.setState({
fullScreen : false
});
}
handleEnterFullscreen = () => {
this.setState({
fullScreen : true
});
}
<VideoPlayer
source = {{ uri: link }}
disableVolume
disableBack
onEnterFullscreen = {this.handleEnterFullscreen}
onExitFullscreen = {this.handleExitFullScreen}
toggleResizeModeOnFullscreen = {false}
/>

If you look at the react-native-video-controls module,
_toggleFullscreen() {
let state = this.state;
state.isFullscreen = ! state.isFullscreen;
if (this.props.toggleResizeModeOnFullscreen) {
state.resizeMode = state.isFullscreen === true ? 'cover' : 'contain';
}
if (state.isFullscreen) {
typeof this.events.onEnterFullscreen === 'function' && this.events.onEnterFullscreen();
}
else {
typeof this.events.onExitFullscreen === 'function' && this.events.onExitFullscreen();
}
this.setState( state );
}
you can see that it executes setSate when changing the status of the screen. That means being rendered again.
The implementations are included in such renderers as react-native-dom, react-native.
Looking at the setState implementation in React.Component, everything was delegated to act on the renderer that created the component instance.
// A bit simplified
setState(partialState, callback) {
// Use the 'updater' field to talk back to the renderer!
this.updater.enqueueSetState(this, partialState, callback);
};
This is how this.setState() is defined in the React package, but it is how DOM is updated.
Read this.updater set by React DOM, allow ReactDOM schedule, and process updates.

Related

useState keeps the previous state

In my app I click Play button and audio should start playing, when it's over speech recognition should start listening, when it's over the message should be printed in console (it will be change to some other actions further).
My app also has a feature to stop recognition process if I click stop button, for that I use useState where false state changes to true.
But, I face an issue: when I try to stop listening by clicking Stop button, the state changes to true and it should stop here, but despite that I have if(isPlaying === false) condition, after true I get false in console as last action 🤷🏻‍♂️ Why it happens so, and how to fix it?
const [song] = useState(typeof Audio !== 'undefined' && new Audio())
const [isPlaying, setIsPlaying] = useState(false)
function playSong() {
setIsPlaying(!isPlaying)
if (listening) {
SpeechRecognition.stopListening()
}
if (isPlaying === false) {
song.src = 'https://raw.songToPlay.mp3'
song.play()
console.log('Song is playing: ' + isPlaying)
song.addEventListener('ended', () => {
console.log('Recognition is started: ' + isPlaying)
SpeechRecognition.startListening()
})
recognition.addEventListener('audioend', () => {
console.log('Recognition is finished: ' + isPlaying)
})
} else {
console.log('When stop is clicked: ' + isPlaying)
}
}
return (
<div>
<p>Microphone: {listening ? 'on' : 'off'}</p>
<button onClick={playSong}>{isPlaying ? 'Stop' : 'Play'}</button>
<p>{transcript}</p>
</div>
)
Output:
Logging the state from within playSong will log the state when the function was defined. This means that the value will be outdated, i.e. stale. This is because of a functional concept called closures. From Wikipedia “Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.”
So how should we log the current state when it is changed? By using the useEffect hook. This function will be re-run whenever any of the dependencies change.
You can also return a 'cleanup' function to remove listeners, etc.
CodeSandbox
const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
if (!isPlaying) {
console.log("Starting to play");
// add your event listener here to wait for song to end and start speech recognition
} else {
console.log("When stop is clicked: " + isPlaying);
// stop your speech recognition here
}
return () => {
// this is a cleanup function - you can use it to remove event listeners so you don't add them twice
};
}, [isPlaying]);
return (
<div>
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? "Stop" : "Play"}
</button>
</div>
);

How to set window resize event listener value to React State?

This issue is very simple but I probably overlook very little point. Window screen size is listening by PostLayout component. When window width is less than 768px, I expect that isDesktopSize is false. I tried everything like using arrow function in setIsDesktopSize, using text inside of true or false for state value, using callback method etc... but it's not working.
PostLayout shared below:
import React, {useState,useEffect, useCallback} from 'react'
import LeftSideNavbar from './LeftSideNavbar'
import TopNavbar from './TopNavbar'
export default function PostLayout({children}) {
const [isDesktopSize, setIsDesktopSize] = useState(true)
let autoResize = () => {
console.log("Desktop: " + isDesktopSize);
console.log(window.innerWidth);
if(window.innerWidth < 768 ){
setIsDesktopSize(false)
}else{
setIsDesktopSize(true)
}
}
useEffect(() => {
window.addEventListener('resize', autoResize)
autoResize();
}, [])
return (
<>
<TopNavbar isDesktopSize={isDesktopSize}/>
<main>
<LeftSideNavbar/>
{children}
</main>
</>
)
}
console log is shared below:
Desktop: true
627
This could probably be extracted into a custom hook. There's a few things you'd want to address:
Right now you default the state to true, but when the component loads, that may not be correct. This is probably why you see an incorrect console log on the first execution of the effect. Calculating the initial state to be accurate could save you some jank/double rendering.
You aren't disconnecting the resize listener when the component unmounts, which could result in an error attempting to set state on the component after it has unmounted.
Here's an example of a custom hook that addresses those:
function testIsDesktop() {
if (typeof window === 'undefined') {
return true;
}
return window.innerWidth >= 768;
}
function useIsDesktopSize() {
// Initialize the desktop size to an accurate value on initial state set
const [isDesktopSize, setIsDesktopSize] = useState(testIsDesktop);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
function autoResize() {
setIsDesktopSize(testIsDesktop());
}
window.addEventListener('resize', autoResize);
// This is likely unnecessary, as the initial state should capture
// the size, however if a resize occurs between initial state set by
// React and before the event listener is attached, this
// will just make sure it captures that.
autoResize();
// Return a function to disconnect the event listener
return () => window.removeEventListener('resize', autoResize);
}, [])
return isDesktopSize;
}
Then to use this, your other component would look like this (assuming your custom hook is just in this same file -- though it may be useful to extract it to a separate file and import it):
import React, { useState } from 'react'
import LeftSideNavbar from './LeftSideNavbar'
import TopNavbar from './TopNavbar'
export default function PostLayout({children}) {
const isDesktopSize = useIsDesktopSize();
return (
<>
<TopNavbar isDesktopSize={isDesktopSize}/>
<main>
<LeftSideNavbar/>
{children}
</main>
</>
)
}
EDIT: I modified this slightly so it should theoretically work with a server-side renderer, which will assume a desktop size.
Try this, you are setting isDesktopSizze to 'mobile', which is === true
const [isDesktopSize, setIsDesktopSize] = useState(true)
let autoResize = () => {
console.log("Desktop: " + isDesktopSize);
console.log(window.innerWidth);
if(window.innerWidth < 768 ){
setIsDesktopSize(true)
}else{
setIsDesktopSize(false)
}
}
I didn't find such a package on npm and I thought it would be nice to create one: https://www.npmjs.com/package/use-device-detect. I think it will help someone :)

react setState not updating state in one of my functions

I'm working an a react app with a few forms and I am trying to implement an edit form for input items. The function first opens the list item in a pre-populated form.
The editItem function currently looks like this:
editItem(event) {
event.preventDefault();
const target = event.target.parentNode.parentNode;
const { key } = target.dataset;
const { className } = target;
const currState = { ...this.state[className] };
const currItem = currState.list[key];
for (let i in currItem) {
if (i !== "list" && i !== "hidden") {
currState[i] = currItem[i]
}
}
this.setState({ [className]: currState });
this.hideUnhide({target: {name: className}});
}
I have confirmed with console logs that currState is correctly set with the values that I am looking for, and that I am not having an async issue. I am using this same format to set state in other functions in my app and all of the others are working properly. If I directly mutate state in the same place, I get the behavior I'm looking for (form fields populate), but nothing happens when I use setState.
Link to my github repo: here. The function in question is in App.js.
As Brian Thompson points out in his comment, it turns out that the hideUnhide function call directly after my setState uses setState as well and writes over the first setState call with the previous state:
hideUnhide(event) {
const { name } = event.target;
const currState = { ...this.state[name] };
if (currState.hidden === true) {
currState.hidden = false;
}
this.setState({ [name]: currState });
}
The way to prevent that was to use hideUnhide as a callback to the setState in editItem:
this.setState({ [className]: currState }, () =>
this.hideUnhide({ target: { name: className } })
);
and now everything functions as intended.

Ensure entrance css transition with React component on render

I am trying to build a barebones css transition wrapper in React, where a boolean property controls an HTML class that toggles css properties that are set to transition. For the use case in question, we also want the component to be unmounted (return null) before the entrance transition and after the exit transition.
To do this, I use two boolean state variables: one that controls the mounting and one that control the HTML class. When props.in goes from false to true, I set mounted to true. Now the trick: if the class is set immediate to "in" when it's first rendered, the transition does not occur. We need the component to be rendered with class "out" first and then change the class to "in".
A setTimeout works but is pretty arbitrary and not strictly tied to the React lifecycle. I've found that even a timeout of 10ms can sometimes fail to produce the effect. It's a crapshoot.
I had thought that using useEffect with mounted as the dependency would work because the component would be rendered and the effect would occur after:
useEffect(if (mounted) { () => setClass("in"); }, [mounted]);
(see full code in context below)
but this fails to produce the transition. I believe this is because React batches operations and chooses when to render to the real DOM, and most of the time doesn't do so until after the effect has also occurred.
How can I guarantee that my class value is change only after, but immediately after, the component is rendered after mounted gets set to true?
Simplified React component:
function Transition(props) {
const [inStyle, setInStyle] = useState(props.in);
const [mounted, setMounted] = useState(props.in);
function transitionAfterMount() {
// // This can work if React happens to render after mounted get set but before
// // the effect; but this is inconsistent. How to wait until after render?
setInStyle(true);
// // this works, but is arbitrary, pits UI delay against robustness, and is not
// // tied to the React lifecycle
// setTimeout(() => setInStyle(true), 35);
}
function unmountAfterTransition() {
setTimeout(() => setMounted(false), props.duration);
}
// mount on props.in, or start exit transition on !props.in
useEffect(() => {
props.in ? setMounted(true) : setInStyle(false);
}, [props.in]);
// initiate transition after mount
useEffect(() => {
if (mounted) { transitionAfterMount(); }
}, [mounted]);
// unmount after transition
useEffect(() => {
if (!props.in) { unmountAfterTransition(); }
}, [props.in]);
if (!mounted) { return false; }
return (
<div className={"transition " + inStyle ? "in" : "out"}>
{props.children}
</div>
)
}
Example styles:
.in: {
opacity: 1;
}
.out: {
opacity: 0;
}
.transition {
transition-property: opacity;
transition-duration: 1s;
}
And usage
function Main() {
const [show, setShow] = useState(false);
return (
<>
<div onClick={() => setShow(!show)}>Toggle</div>
<Transition in={show} duration={1000}>
Hello, world.
</Transition>
<div>This helps us see when the above component is unmounted</div>
</>
);
}
Found the solution looking outside of React. Using window.requestAnimationFrame allows an action to be take after the next DOM paint.
function transitionAfterMount() {
// hack: setTimeout(() => setInStyle(true), 35);
// not hack:
window.requestAnimationFrame(() => setInStyle(true));
}

React useCallback does not get updated on state change

The sample below is a simplified excerpt where a child component emits events based on mouse behaviours. React then should update the DOM according to the emitted events.
function SimpleSample() {
const [addons, setAddons] = React.useState<any>({
google: new GoogleMapsTile('google'),
})
const [tooltip, setTooltip] = React.useState<null | { text: string[]; x; y }>(null)
React.useEffect(() => {
// ...
}, [])
const mapEventHandle = React.useCallback(
(event: MapEvents) => {
console.log('event', event.type, tooltip) // LOG 1
if (event.type === MapEventType.mouseoverPopup_show) {
setTooltip({ text: event.text, x: event.coordinates[0], y: event.coordinates[1] })
} else if (event.type === MapEventType.mouseoverPopup_move) {
if (tooltip) setTooltip({ ...tooltip, x: event.coordinates[0], y: event.coordinates[1] })
} else if (event.type === MapEventType.mouseoverPopup_hide) {
setTooltip(null)
}
},
[tooltip]
)
console.log('render', tooltip) // LOG 2
return <MapComponent addons={addons} onEvent={mapEventHandle} />
}
The following order of events is expected:
mouseoverPopup_show is emitted, then tooltip changed from null to a value, a rerender occurs
mouseoverPopup_move is emitted, then tooltip is updated, triggering a rerender
What actually is happening:
Logpoint LOG 2 logs the updated value of tooltip (correct)
When mapEventHandle is called again, the value of tooltip inside that closure (logpoint LOG 1) is never updated, being always null.
Am I missing somethinig? Using the wrong hook?
Here's a codesandbox for it
https://codesandbox.io/s/blissful-torvalds-wm27f
EDIT: On de codesandbox sample setTooltip is not even triggering a rerender
Thanks for the help folks, the issue seems to be down inside a dependency of <MapComponent/>. It ended up saving a reference to the old callback on construction. Still a caveat to watch for, and which i probably wouldnt face with class components...
//something like this
class MapComponent {
emitter = this.props.onChange //BAD
emitter = (...i) => this.props.onChange(...i) //mmkay
}
I think event.coordinates is undefined so event.coordinates[0] causes an error.
If you do: setTooltip({working:'fine'}); you'll get type errors but it does set the toolTip state and re renders.
Thanks to your answer it helped me debug mine which was a bit different. Mine was not working because the callback reference was kept in a state value of the child component.
const onElementAdd = useCallBack(...)..
<Dropzone onElementAdded={props.onElementAdded} />
export const Dropzone = (props: DropzoneProps): JSX.Element => {
const [{ isOver }, drop] = useDrop(
() => ({
accept: props.acceptedTypes,
drop: (item: BuilderWidget, monitor): void => {
if (monitor.didDrop()) return;
props.onElementAdded(item);
},
}),
// missed props.onElementAdded here
[props.onElementAdded, props.acceptedTypes, props.disabled],
);

Resources