How can I add an event to a child component? - javascript

I'd like certain elements on my site to emit a sound when clicked. I easily did that by adding an audio element with a ref, and a function that plays the refs sound onClick.
const sound = require('../sounds/bird.mp3');
const soundRef = useRef(null);
const playSound = () => {
soundRef.current.play();
}
return <div>
<audio ref={soundRef}><source src={sound} /></audio>
<img src="bird.png" onClick={playSound} />
</div>
Since I now found out I may need to reuse this, I decided to create an Audio component to encapsulate the clicked element. I'd like it to receive the sound's file name as a property:
<Audio soundName="bird">
<div><img src="bird.png" /></div>
</Audio>
My problem is: in the Audio component, I get the soundName and children props and render the {children, but how do I add an onClick even to the child element, so it would trigger the Audio element playSound function?
What I currently have in the Audio component:
export default ({soundName, ...children}) => {
const sound = require(`../sounds/${soundName}.mp3`);
const soundRef = useRef(null);
const playSound = () => {
soundRef.current.play();
}
return <>
<audio ref={soundRef}><source src={sound} /></audio>
{children} // <--- this is where I need to somehow add the event
</>
}
I'm hoping I'm missing a tiny thing here...

You can wrap your children with something, like a div, who can listen for your event, something like:
export default ({soundName, ...children}) => {
const sound = require(`../sounds/${soundName}.mp3`);
const soundRef = useRef(null);
const playSound = () => {
soundRef.current.play();
}
return <>
<audio ref={soundRef}><source src={sound} /></audio>
<div onClick={ playSound }>
{children} // <--- this is where I need to somehow add the event
</div>
</>
}
I think this covers most of your cases.
There might be a children which intercept the event before the Audio component and prevent the event propagation to the parent, something like:
<Audio soundName="bird">
<img
src="bird.png"
onClick={ event => {
doSomethingOnThisImageClick()
event.stopPropagation()
}}
/>
</Audio>
If you know that this case might happen but you still want to play the file no matter what, you need to recognize the event in its capture phase, BEFORE the child component even know it, using the Capture variant of the event name, like:
export default ({soundName, ...children}) => {
const sound = require(`../sounds/${soundName}.mp3`);
const soundRef = useRef(null);
const playSound = () => {
soundRef.current.play();
}
return <>
<audio ref={soundRef}><source src={sound} /></audio>
{ /* notice here? */ }
{ /* | */ }
{ /* v */ }
<div onClickCapture={ playSound }>
{children}
</div>
</>
}

Related

React Lifecycle Video

I was creating this React component and I was astonished by the fact that when I clicked on the video I got false on console immediatelly after the video started. I was expecting that it would print true when the video started, and false when it finished. I think I'm confusing the lifecycle of this particular component, and I would be really grateful if someone could clarify this to me.
const Video = ({src, type, index, isAutoPlay}) => {
const [play, setPlay] = useState(isAutoPlay)
const playRef = useRef();
useEffect(() => {
if (play && playRef.current) {
playRef.current.play();
}
return () => setPlay(false)
}, [play]);
return (
<>
<video
className="slide"
ref={playRef}
onClick={() => {
setPlay(true);
console.log(play);}}>
<source src={src} type={type} key={index}/>
Your browser does not support the video tag.
</video>
</>
)
}

Pause other video if selected video is playing in react

I'm using react-player https://github.com/cookpete/react-player to play my videos. My problem is, how can I pause other videos while selected video is playing?
const videoRef = useRef();
const updateVideoHandler = async (videoId, videoTitle) => {
setSelectedVideoId(videoId);
if (!selectedVideoId) {
videoRef?.current?.player?.player?.onPause();
}
};
<ReactPlayer
ref={videoRef}
onPlay={() => updateVideoHandler(video.id, video.title)}
playsinline={true}
playing={true}
controls={true}
url={video?.url}
width="100%"
height="100%"
playIcon={
<div
className="play-icon"
role="button"
tabIndex={0}
style={{ outline: "none" }}
>
{" "}
<img src="/images/play.png" alt="" />
</div>
}
light={video?.pic}
/>;
You could store all player instances in a Context and use a Provider and Consumer to pause all players if one starts playing.
Since you pass a playing boolean to ReactPlayer, you can easily store a id or reference of the current playing player.
For example:
PlayerProvider.jsx
export const PlayerContext = React.createContext({
play: (playerId) => true,
pause: (playerId) => true,
isPlaying: (playerId) => false,
});
function PlayerProvider({ children }) {
// store the id of the current playing player
const [playing, setPlaying] = useState('');
// set playing to the given id
const play = playerId => setPlaying(playerId);
// unset the playing player
const pause = () => setPlaying(false);
// returns true if the given playerId is playing
const isPlaying = playerId => playerId === playing;
return (
<PlayerContext.Provider value={{ play, pause, isPlaying }}>
{children}
</PlayerContext.Provider>
)
}
export default PlayerProvider;
Player.jsx
import { PlayerContext } from './PlayerProvider';
function Player({ video, id }) {
const { isPlaying, play, pause } = useContext(PlayerContext);
<ReactPlayer
ref={videoRef}
playsinline={true}
playing={isPlaying(id)}
controls={true}
url={video?.url}
width="100%"
height="100%"
onPause={() => pause(id)}
onEnded={() => pause(id)}
onClickPreview={() => play(id)}
playIcon={
<div
className="play-icon"
role="button"
tabIndex={0}
style={{ outline: "none" }}
>
{" "}
<img src="/images/play.png" alt="" />
</div>
}
light={video?.pic}
/>;
}
export default Player;
Page.jsx
import PlayerProvider from './PlayerProvider';
import Player from './Player';
function Page() {
return (
<PlayerProvider>
<Player video="/path/to/video1.mp4" id="player1" />
<Player video="/path/to/video2.mp4" id="player2" />
<Player video="/path/to/video3.mp4" id="player3" />
</PlayerProvider>
)
}
Actially this is very easy process to get
try it out
export const VideoPlayer = () =>{
const videoooo = useRef();
const pauseVideo = () => {
//at the place of pauseVideo you can use "stopVideo", "playVideo"
videoooo.current.contentWindow.postMessage(
'{"event":"command","func":"pauseVideo","args":""}',
"*"
);
};
return(<> <iframe
ref={videoooo}
id="myVideoId"
src="https://www.youtube.com/embed/Q63qjIXMqwU?enablejsapi=1"
></iframe>
<button
onClick={() => {
pauseVideo();
}}
>
Pause
</button></>)
}
it is very easy and useful syntax from js
// unset the playing player
const pause = () => setPlaying(false);
if you remove this line from the code it is working perfectly, when one video is already playing and you click on the second video, onPause is invoked and it is calling the pause function due to that setPlaying updating the playing value due to that page rerendering and in that time playing value is false and it does not match with any video id and that's why every video is stopped playing.
I implemented it using video-react here :
react-video-js
You need to use
controls,
preload='auto',
autoplay
Or
You can also pop-up a modal and show videos and use this
<div>
<button onClick={this.playVideo}>
Play!
</button>
<button onClick={this.pauseVideo}>
Pause!
</button>
</div>
where you need to store the states inside these onClick for play and pause depending upon the useref of a particular video else if you use a Modal then destroy it after close so that video doesn't play once you close.

How to play one song from array at a time react js

I am building a simple music player but where I fail is at trying to execute one item from the array at a time. I am using React H5 Audio Player package to play the music. I am currently mapping through all the songs but I don't know how to properly play one at a time. I appreciate any help. I've been stuck on this for a few days.
import { SongContext } from '../../SongContext';
import AudioPlayer from 'react-h5-audio-player';
import 'react-h5-audio-player/lib/styles.css';
import './Player.css';
const Player = () => {
const { songs } = useContext(SongContext);
return (
<>
{songs.length > 0 &&
songs.map((song) => (
<div className="player" key={song.id}>
<AudioPlayer
// autoPlay
// src={song.preview}
showJumpControls={false}
customVolumeControls={[]}
customAdditionalControls={[]}
onPlay={() => console.log('playing')}
/>
</div>
))}
</>
);
};
export default Player;
Don't map all the songs at once, take a song by index (currentSong), and when it's done, use the onEnded event to increment the index, so the next one would play.
Example (codepen):
const Player = () => {
const { songs } = useContext(SongContext);
const [currentSong, setCurrentSong] = useState(0);
const song = songs[currentSong];
if(!song) return null; // don't render the player when no song is available
return (
<div className="player">
<AudioPlayer
autoPlay
src={song.preview}
showJumpControls={false}
customVolumeControls={[]}
customAdditionalControls={[]}
onPlay={() => console.log('playing')}
onEnded={() => setCurrentSong(i => i + 1)}
/>
</div>
);
};

How to pass state from hook to display in App hook in React?

How do I send the state(data) of info to the App hook, so that I can display the properties in divs? setInfo(info) stores the data I want to display on the Map hook and I can see all the properties when I console.log(info) in _onClick. I am stuck on how to use and show it in App. Thanks for the help.
App
const App = () => {
return (
<div className="App">
<div className="inner-left map-container">
<Map />
</div>
<div className="inner-right info-container">
<Nav />
<Search />
<div id="info_side">
{info} // throws an error undefined
</div>
</div>
</div>
);
}
export default App;
Map
const Map = () => {
...
const [info, setInfo] = useState(null)
...
const _onClick = event => {
const { features } = event;
const info = features && features.find(f => f.layer.id === 'icon');
setInfo(info); // This is what I would like to use in App
}
return (
<ReactMapGL
{...viewport}
onViewportChange={_updateViewport}
width="100%"
height="100%"
mapStyle={mapStyle}
mapboxApiAccessToken={TOKEN}
onHover={_onHover}
onClick={_onClick}>
<Source id="my-data" type="geojson" data={geojson}>
<Layer {...icon} />
</Source>
<div style={navStyle}>
<NavigationControl onViewportChange={_updateViewport} />
...
</div>
</ReactMapGL>
);
}
export default Map;
You can pass a callback prop to the child.
App
const App = () => {
const [info, setInfo] = useState(); // local info state
const displayInfo = () => {
// this is where I would hope to return the <div>'s with data such as <div>info.Company</div>
// access current copy of info
}
return (
<div className="App">
<div className="inner-left map-container">
<Map onInfoChange={setInfo} /> // pass info state setter to child
</div>
<div className="inner-right info-container">
<Nav />
<Search />
<div id="info_side">
{displayInfo}
</div>
</div>
</div>
);
}
export default App;
Map
const Map = ({
onInfoChange, // passed callback prop
}) => {
...
const [info, setInfo] = useState(null)
// use an effect hook to call callback whenever info updates
useEffect(() => {
onInfoChange(info); // pass info back to parent
}, [info]);
const _onClick = event => {
const { features } = event;
const info = features && features.find(f => f.layer.id === 'icon');
setInfo(info); // This is what I would like to use in App
}
return (
<ReactMapGL
{...viewport}
onViewportChange={_updateViewport}
width="100%"
height="100%"
mapStyle={mapStyle}
mapboxApiAccessToken={TOKEN}
onHover={_onHover}
onClick={_onClick}>
<Source id="my-data" type="geojson" data={geojson}>
<Layer {...icon} />
</Source>
<div style={navStyle}>
<NavigationControl onViewportChange={_updateViewport} />
...
</div>
</ReactMapGL>
);
}
export default Map;
At this point I should point out that you now have duplicate state stored in two components so it's a great idea to hoist the info state and logic to the closest common ancestor (App) and simply pass the info as a prop to the component that needs it (Map). Single source of truth principle. Or as other have pointed out, switch to a global state management system, like redux, or roll your own using React Context (i.e. what react-redux is using under the hood).
If several components share state, the state should be managed by the first common ancestor and passed down via properties. In your case, App should manage the state and pass what you need to Map as props.
But how do we update the parent state based on a child event? Create the event handler on the parent and pass it to the child component as a callback function via props.
For example, on your app create the callback function function:
const [info, setInfo] = useState({});
const handleEvent = event => {
// Do whatever you need here
setInfo(info);
}
Pass info (the state) and the callback function to the child via props:
<Map info={info} clicked={handleEvent}/> //Do not use handleEvent(), otherwise it will call the function immediately
On Map, use the props and set clicked as event listener:
const Map = ({info, clicked}) => { // destructuring pros.info and props.clicked
<ReactMapGL
{...viewport}
onViewportChange={_updateViewport}
width="100%"
height="100%"
mapStyle={mapStyle}
mapboxApiAccessToken={TOKEN}
onHover={_onHover}
onClick={clicked}>
See more info here
https://towardsdatascience.com/passing-data-between-react-components-parent-children-siblings-a64f89e24ecf

react useCallback hook unexpected behavior on re-render

Maybe I just don't get how hooks are supposed to work, but I have tried many different configurations to convert this component to hooks and they all fail.
export const VideoContainer = ({ localStream, remoteStream, status }: Props) => {
const classes = useStyles();
const setLocalStreamEl = useCallback(node => {
console.log('useCallback');
if (localStream && node && !node.srcObject) {
console.log('setting srcObject');
node.srcObject = localStream;
}
}, []);
return (
<InjectIntl>
{intl =>
localStream && remoteStream && status === intl.messages.inProgress ? (
<div className={classes.videoDiv}>
render something else
</div>
) : (
<div className={classes.videoDiv}>
<video ref={setLocalStreamEl} className={classes.mainStream} autoPlay controls />
</div>
)
}
</InjectIntl>
);
};
I get the video element rendering with the video stream, but the status prop gets renewed every second and on a re-render it calls all of the useCallback logic and it causes the video to flicker annoyingly.
I have tried adding a useRef callback, and different combinations of what is below and useEffect, and I've read through the docs 100 times but I can't fit all the pieces together in a way that works and doesn't reset the ref on every render.
Any ideas?

Categories