i have a that i want to play a very small mp3 once that is in my public folder in my react app, there are so many different solutions to playing audio but none that are simple for react, i tried react-360 but there was a problem with one of the modules.
i've tried some react audio libraries, but they seem to be for audio players, i dont need a play button and a pause and a volume and all that. just want a simple sound effect
class App extends Component {
render() {
var audio = new Audio("../public/sound.mp3")
return (
<Container>
<img src={dwight} onClick={ audio.play() }/>
</Container>
);
}
}
(sound to play when click the image)
Hooks version (React 16.8+):
Minimal version.
Place your music file (mp3) in the public folder.
import React from 'react';
function App() {
let audio = new Audio("/christmas.mp3")
const start = () => {
audio.play()
}
return (
< div >
<button onClick={start}>Play</button>
</div >
);
}
export default App;
You can import the audio using import statement and create a Audio object and call the play() method using the instantiated object.
import React from "react";
import audio from './../assets/audios/success.mp3';
class AudioTest extends React.Component{
playAudio = () => {
new Audio(audio).play();
}
render() {
return (
<div>
<button onClick={this.playAudio}>PLAY AUDIO</button>
</div>
);
}
}
export default AudioTest;
You need to pass a function thats trigger play:
<Container>
<img src={dwight} onClick={ () => audio.play() }/>
</Container>
Add to your render method<audio id="audio"><source src="slow-spring-board.mp3" type="audio/mp3"></source></audio>
And inside the script document.getElementById('audio').play()
<Container>
<img src={dwight} onClick={audio.play}/>
</Container>
By passing audio.play as the onClick handler, rather than audio.play(), you're passing the play function itself. audio.play() will instead invoke the function - if it also returned a function for some reason, then that function would be invoked on click.
so i moved the sound and picture files INSIDE of the src folder, into assets, THEN changed the onClick function to a combo that finally worked, it looks like a double invocation did the trick
class App extends Component {
render() {
return (
<Container>
<img src={dwight} alt="ds" onClick={() => audio.play()} />
<h1> PRESS ME</h1>
</Container>
);
}
}
export default App;
import { Fragment } from 'react';
export default function PlaySound(props) {
var savingCurrencySound = props.soundFile ?? 'https://example.com/audio/sound.mp3';
var playCurrencySound = () => {
var audio = new Audio(savingCurrencySound);
audio.play();
}
return (
<Fragment>
<img src={dwight} onClick={ () => playCurrencySound() }/>
</Fragment>
);
}
According to this answer
here's the following worked code :
import React, { useState, useEffect } from "react";
const useAudio = url => {
const [audio] = useState(new Audio(url));
const [playing, setPlaying] = useState(false);
const toggle = () => setPlaying(!playing);
useEffect(() => {
playing ? audio.play() : audio.pause();
},
[playing]
);
useEffect(() => {
audio.addEventListener('ended', () => setPlaying(false));
return () => {
audio.removeEventListener('ended', () => setPlaying(false));
};
}, []);
return [playing, toggle];
};
const Player = ({ url = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3' }) => {
const [playing, toggle] = useAudio(url);
return (
<div>
<button onClick={toggle}>{playing ? "Pause" : "Play"}</button>
</div>
);
};
export default Player;
or you can use this playground here
Related
0
I have a list of messages and I am trying to get some link from that message if it exists there (and store it in studentlink const) this will happen in an event listener. getElemenBy tag and id and the name don't work, I don't know why! what is your idea? in the next step, I need to run that link whit out clicking, automatically but I didn't find any method or solution yet, how is possible?
import React from "react";
import { connect } from "react-redux";
import reactDom from "react-dom";
function MessageList(props) {
const handleLoad = (e) => {
if (listItems !== "") {
let studentlink = document.getElementById("myAnchor").href;
window.location.href = studentlink;
alert(`${studentlink}`);
} else {
console.log(window.location.href);
}
};
React.useEffect(() => {
window.addEventListener("load", handleLoad);
// cleanup this component
return () => {
window.removeEventListener("load", handleLoad);
};
}, []);
const arr = props.studentmessage;
const listItems = arr.map((message, index) => (
<li key={index} id="myAnchor">
{message}
</li>
));
return (
<section>
<div className="commentbox">
<ul>{listItems}</ul>
</div>
</section>
);
}
const mapStatetoprops = (state) => {
return {
studentmessage: state.message,
};
};
export default connect(mapStatetoprops)(MessageList);
I have a component in React that takes a URL and shows a button that lets the user stop/pause an audio file. This component is used multiple times on the page.
The button starts the audio just fine, however it audio.pause() does not stop the stream.
I am using Next.js, however I am importing the component like so:
const HistoryRow = dynamic(() => import('../components/HistoryRow'), { ssr: false });
Anyone know why this doesn't stop the stream? I can confirm it's entering the if statement just fine.
import { useState } from 'react';
import m from 'moment';
import path from 'path';
function HistoryRow({ data }) {
const [playing, setPlaying] = useState(false);
const audio = new Audio(`${window.location.origin}/audio-responses/${data.timestamp}.wav`);
audio.onended = function () {
setPlaying(false);
};
audio.onpause = function () {
setPlaying(false);
};
audio.onplay = function () {
setPlaying(true);
};
audio.onerror = function () {
setPlaying(false);
};
function execute() {
if (playing) {
audio.pause();
} else {
audio.play();
}
}
const stopSvg = (
<svg> /** svg data ** / </svg>
);
const playSvg = (
<svg> /** svg data ** / </svg>
);
return (
<div className="text-base flex leading-5 font-medium text-blue-600 truncate" onClick={() => execute()}>
{data.type === 'command' ? (playing ? stopSvg : playSvg) : null}
</div>
);
}
export default HistoryRow;
Whenever you state changes, the whole component is rerendered. This means that every variable in the component is rewritten. So you create an Audio instance, then start playing, the component rerenders and a new Audio instance is created en therefor you've lost the binding to the previous instance.
You can use the useRef hook to create a reference to the Audio instance that persists over the entire lifespan of the component. So it will never change unless you explicitly tell it to. You can access the instance with the current property on the returned useRef value.
Since React is state driven I would suggest using the useEffect hook to listen for changes in the playing state and either start or stop playing based on the value of the playing state, that's the other way around as you have it currently.
function HistoryRow({ data }) {
const [playing, setPlaying] = useState(false);
const [hasError, setHasError] = useState(false);
const audio = useRef(new Audio(`${window.location.origin}/audio-responses/${data.timestamp}.wav`));
audio.current.onended = function () {
setPlaying(false);
};
audio.current.onplay = function () {
setHasError(false);
};
const handleClick = () => {
setPlaying(playing => !playing);
});
useEffect(() => {
if (playing) {
audio.current.play().then(() => {
// Audio is playing.
}).catch(error => {
setHasError(true);
});
} else if (!hasError) {
audio.current.pause();
}
}, [playing, hasError]);
const stopSvg = (
<svg> /* svg data */ </svg>
);
const playSvg = (
<svg> /* svg data */ </svg>
);
return (
<div
className="text-base flex leading-5 font-medium text-blue-600 truncate"
onClick={handleClick}>
{data.type === 'command' ? (playing ? stopSvg : playSvg) : null}
</div>
);
}
I am wondering if there is a good way to prevent the flashing of the fallback in react. I am using react router and the issue is that when a component gets suspended the fallback loader flashes really quick and it is pretty annoying. I saw the answer here React suspense/lazy delay? which would look like the following:
const Home = lazy(() => {
return Promise.all([
import('./components/Home'),
new Promise(resolve => setTimeout(resolve, 500))
]).then(([moduleExports]) => moduleExports);
});
but my issue with this is that I have an overlay loading spinner with a transparent background and the component doesn't actually load until the promises are resolved. This leaves the page hanging without content for a half of a second and is actually more annoying then the flashing of the spinner.
So I guess the question is has anyone found a good way to deal with this issue. I would really like to add something like nprogress to the page but can't figure out how I would implement this with React.suspense. I may just have to go back to using react loadable but I really don't want to when react comes with basically the same functionality out of the box.
I recently was facing the same issue and I ended up with this implementation of a component that I use for the fallback of the suspense.
The idea is simple, just don't show anything for a certain amount of time, then show the loader. So let's say for like 300ms there won't be anything displayed, after the delay it should display (in this case) the ContentLoader or anything you like.
In Typescript as lazy-loader.ts
import { FC, useEffect, useState } from 'react';
import ContentLoader from './content-loader'; // or any spinner component
export interface LazyLoaderProps {
delay?: number;
}
const LazyLoader: FC<LazyLoaderProps> = ({
delay = 250,
...props
}) => {
const [show, setShow] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => {
setShow(true);
}, delay);
return () => {
clearTimeout(timeout);
};
}, [delay]);
return show ? <ContentLoader {...props} /> : null;
};
export { LazyLoader as default };
then use like so
import LazyLoader from "./lazy-loader"
// ...
<Suspense fallback={<LazyLoader delay={300} />}>...</Suspense>
That does not delay the import. (which I also thought was not optimal)
Let me know if this helps.
import React, { Suspense, lazy } from "react";
const Home = lazy(() => {
return Promise.all([
import("./home"),
new Promise(resolve => setTimeout(resolve, 300))
]).then(([moduleExports]) => moduleExports);
});
function FullSpinner() {
return (
{/** full spinner jsx goes here */}
<div className="full-spinner">
<p>loading....</p>
</div>
)
}
function App() {
return (
<div className="App">
<h1>app component</h1>
<Suspense fallback={<FullSpinner />}>
<Home />
</Suspense>
</div>
);
}
Edit:
import React, { Suspense, lazy, useState, useEffect } from "react";
const Home = lazy(() => {
return Promise.all([
import("./home"),
new Promise(resolve => setTimeout(resolve, 500))
]).then(([moduleExports]) => moduleExports);
});
function FullSpinner() {
return (
<div className="full-spinner">
<p>loading....</p>
</div>
);
}
const LazyLoading = ({ delay, loader: Loader, children }) => {
const [ready, setReady] = useState(false);
useEffect(() => {
setTimeout(() => setReady(true), delay);
}, [delay]);
return ready ? (
<Suspense fallback={<Loader />}>{children}</Suspense>
) : (
<Loader />
);
};
function App() {
return (
<div className="App">
<h1>app component</h1>
<LazyLoading delay={2000} loader={FullSpinner}>
<Home />
</LazyLoading>
</div>
);
}
import React, { Component } from 'react'
import { Button, Input, Icon,Dropdown,Card} from 'semantic-ui-react'
import { Link } from 'react-router-dom'
import $ from 'jquery'
import styles from './Home.scss'
import Modal from './Modal.jsx'
import MakeChannelModal from './MakeChannelModal.jsx'
class Music extends React.Component {
constructor(props) {
super(props);
this.state = {
play: false,
pause: true
};
this.url = "http://streaming.tdiradio.com:8000/house.mp3";
this.audio = new Audio(this.url);
}
play(){
this.setState({
play: true,
pause: false
});
console.log(this.audio);
this.audio.play();
}
pause(){
this.setState({ play: false, pause: true });
this.audio.pause();
}
render() {
return (
<div>
<button onClick={this.play}>Play</button>
<button onClick={this.pause}>Pause</button>
</div>
);
}
}
export default Music
This is the code that I am using to play the sound with url (this.url) in my react app. When I press the play button, it gives me an error
Uncaught TypeError: Cannot read property 'setState' of undefined
I am not sure why this is happpening since I don't see any undefined states. A;; states have been declared.
I am new to react so I might be missing something very important.
Please help!
ES6 class properties syntax
class Music extends React.Component {
state = {
play: false
}
audio = new Audio(this.props.url)
componentDidMount() {
audio.addEventListener('ended', () => this.setState({ play: false }));
}
componentWillUnmount() {
audio.removeEventListener('ended', () => this.setState({ play: false }));
}
togglePlay = () => {
this.setState({ play: !this.state.play }, () => {
this.state.play ? this.audio.play() : this.audio.pause();
});
}
render() {
return (
<div>
<button onClick={this.togglePlay}>{this.state.play ? 'Pause' : 'Play'}</button>
</div>
);
}
}
export default Music;
Hooks version (React 16.8+):
import React, { useState, useEffect } from "react";
const useAudio = url => {
const [audio] = useState(new Audio(url));
const [playing, setPlaying] = useState(false);
const toggle = () => setPlaying(!playing);
useEffect(() => {
playing ? audio.play() : audio.pause();
},
[playing]
);
useEffect(() => {
audio.addEventListener('ended', () => setPlaying(false));
return () => {
audio.removeEventListener('ended', () => setPlaying(false));
};
}, []);
return [playing, toggle];
};
const Player = ({ url }) => {
const [playing, toggle] = useAudio(url);
return (
<div>
<button onClick={toggle}>{playing ? "Pause" : "Play"}</button>
</div>
);
};
export default Player;
Update 03/16/2020: Multiple concurrent players
In response to #Cold_Class's comment:
Unfortunately if I use multiple of these components the music from the other components doesn't stop playing whenever I start another component playing - any suggestions on an easy solution for this problem?
Unfortunately, there is no straightforward solution using the exact codebase we used to implement a single Player component. The reason is that you somehow have to hoist up single player states to a MultiPlayer parent component in order for the toggle function to be able to pause other Players than the one you directly interacted with.
One solution is to modify the hook itself to manage multiple audio sources concurrently. Here is an example implementation:
import React, { useState, useEffect } from 'react'
const useMultiAudio = urls => {
const [sources] = useState(
urls.map(url => {
return {
url,
audio: new Audio(url),
}
}),
)
const [players, setPlayers] = useState(
urls.map(url => {
return {
url,
playing: false,
}
}),
)
const toggle = targetIndex => () => {
const newPlayers = [...players]
const currentIndex = players.findIndex(p => p.playing === true)
if (currentIndex !== -1 && currentIndex !== targetIndex) {
newPlayers[currentIndex].playing = false
newPlayers[targetIndex].playing = true
} else if (currentIndex !== -1) {
newPlayers[targetIndex].playing = false
} else {
newPlayers[targetIndex].playing = true
}
setPlayers(newPlayers)
}
useEffect(() => {
sources.forEach((source, i) => {
players[i].playing ? source.audio.play() : source.audio.pause()
})
}, [sources, players])
useEffect(() => {
sources.forEach((source, i) => {
source.audio.addEventListener('ended', () => {
const newPlayers = [...players]
newPlayers[i].playing = false
setPlayers(newPlayers)
})
})
return () => {
sources.forEach((source, i) => {
source.audio.removeEventListener('ended', () => {
const newPlayers = [...players]
newPlayers[i].playing = false
setPlayers(newPlayers)
})
})
}
}, [])
return [players, toggle]
}
const MultiPlayer = ({ urls }) => {
const [players, toggle] = useMultiAudio(urls)
return (
<div>
{players.map((player, i) => (
<Player key={i} player={player} toggle={toggle(i)} />
))}
</div>
)
}
const Player = ({ player, toggle }) => (
<div>
<p>Stream URL: {player.url}</p>
<button onClick={toggle}>{player.playing ? 'Pause' : 'Play'}</button>
</div>
)
export default MultiPlayer
Example App.js using the MultiPlayer component:
import React from 'react'
import './App.css'
import MultiPlayer from './MultiPlayer'
function App() {
return (
<div className="App">
<MultiPlayer
urls={[
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3',
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3',
]}
/>
</div>
)
}
export default App
The idea is to manage 2 parallel arrays:
your audio sources (built from the urls props you pass to the parent component ; the urls props is an array of strings (your MP3 URLs))
an array tracking the state of each player
The toggle method updates the player state array based on the following logic:
if there is a player currently active (i.e. audio is playing) and this active player is not the player targeted by the toggle method, revert that player's playing state to false, and set the targeted player's playing state to true [you clicked on 'play' while another audio stream was already playing]
if the player currently active is the player targeted by the toggle method, simply revert the targeted player's playing state to false [you clicked on 'pause']
if there is no player currently active, simply set the targeted player's state to true [you clicked on 'play' while no audio stream was currently playing]
Note that the toggle method is curried to accept the source player's index (i.e. the index of the child component where the corresponding button was clicked).
Actual audio object control happens in useEffect as in the original hook, but is slightly more complex as we have to iterate through the entire array of audio objects with every update.
Similarly, event listeners for audio stream 'ended' events are handled in a second useEffect as in the original hook, but updated to deal with an array of audio objects rather than a single such object.
Finally, the new hook is called from the parent MultiPlayer component (holding multiple players), which then maps to individual Players using (a) an object that contains the player's current state and its source streaming URL and (b) the toggle method curried with the player's index.
CodeSandbox demo
You can also accomplish this by using the useSound hook.
To do this, first install the npm package:
npm install use-sound
Imports:
import useSound from 'use-sound'
import mySound from '../assets/sounds/yourSound.mp3' // Your sound file path here
Usage example 1
A simple approach..
function MyButton(){
const [playSound] = useSound(mySound)
return (
<button onClick={() => playSound()}>
Play Sound
</button>
)
}
Usage example 2
In this setup we can control the volume. Also, playSound() will be called inside the handleClick() function, allowing you to do more things on click than just playing a sound.
function MyButton(){
const [playSound] = useSound(mySound, { volume: 0.7 }) // 70% of the original volume
const handleClick = () => {
playSound()
// maybe you want to add other things here?
}
return (
<button onClick={() => handleClick()}>
Play Sound
</button>
)
}
For more info click here or here
I faced a different problem with this implementation of the answer.
It seemed the browser was continuously trying to download the sound on every re-render.
I ended up using useMemo for the Audio with no dependencies which causes the hook to only ever once create the Audio and never attempt to recreate it.
import {useMemo, useEffect, useState} from "react";
const useAudio = url => {
const audio = useMemo(() => new Audio(url), []);
const [playing, setPlaying] = useState(false);
const toggle = () => setPlaying(!playing);
useEffect(() => {
playing ? audio.play() : audio.pause();
},
[playing]
);
useEffect(() => {
audio.addEventListener('ended', () => setPlaying(false));
return () => {
audio.removeEventListener('ended', () => setPlaying(false));
};
}, []);
return [playing, toggle];
};
export default useAudio;
I got some problems following these steps when working with Next Js because Audio is HTMLElement tag, eventually, it was rendering me a big fat error, so I decided to study more and the result for it in my project was the following:
//inside your component function.
const [audio] = useState( typeof Audio !== "undefined" && new Audio("your-url.mp3")); //this will prevent rendering errors on NextJS since NodeJs doesn't recognise HTML tags neither its libs.
const [isPlaying, setIsPlaying] = useState(false);
To handle the player, I made a useEffect:
useEffect(() => {
isPlaying ? audio.play() : audio.pause();
}, [isPlaying]);
You will manage the state "isPlaying" according to the functions you make so far.
I'm a bit late to the party here but piggy backing off of 'Thomas Hennes':
One problem people looking at this will run into is, if you try to use this code verbatim in an app with multiple pages, they are not going to have a nice time. Since state is managed at the component, you can play, navigate and play again.
To get around that you want to have your component push it's state up to App.js instead and manage the state there.
Allow me to show what I mean.
My player component looks like this:
import React, { Component } from 'react'
class MusicPlayer extends Component {
render() {
const { playing } = this.props.player;
return (
<div>
<button onClick={this.props.toggleMusic.bind(this, playing)}>{playing ? "Pause" : "Play"}</button>
</div>
);
}
};
export default MusicPlayer;
Then in my App.js it looks something like this (using a TODO list sample app):
import React, { Component } from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom'
import './App.css';
import Header from './componets/layout/Header'
import Todos from './componets/Todos'
import AddTodo from './componets/AddTodo'
import About from './componets/pages/About'
import MusicPlayer from './componets/MusicPlayer'
import axios from 'axios';
class App extends Component {
constructor(props) {
super(props);
this.state = { playing: false, todos: [] }
this.audio = new Audio('<YOUR MP3 LINK HERE>');
}
componentDidMount(){
axios.get('https://jsonplaceholder.typicode.com/todos')
.then(res => this.setState({ playing: this.state.playing, todos: res.data }))
}
toggleComplete = (id) => {
this.setState({ playing: this.state.playing, todos: this.state.todos.map(todo => {
if (todo.id === id){
todo.completed = !todo.completed
}
return todo
}) });
}
delTodo = (id) => {
axios.delete(`https://jsonplaceholder.typicode.com/todos/${id}`)
.then(res => this.setState({ playing: this.state.playing, todos: [...this.state.todos.filter(todo => todo.id !== id)] }));
}
addTodo = (title) => {
axios.post('https://jsonplaceholder.typicode.com/todos', {
title,
completed: false
})
.then(res => this.setState({ playing: this.state.playing, todos: [...this.state.todos, res.data]}))
}
toggleMusic = () => {
this.setState({ playing: !this.state.playing, todos: this.state.todos}, () => {
this.state.playing ? this.audio.play() : this.audio.pause();
});
}
render() {
return (
<Router>
<div className="App">
<div className="container">
<Header />
<Route exact path="/" render={props => (
<React.Fragment>
<AddTodo addTodo={this.addTodo} />
<Todos todos={this.state.todos} toggleComplete={this.toggleComplete} delTodo={this.delTodo} />
</React.Fragment>
)} />
<Route path="/About" render={props => (
<React.Fragment>
<About />
<MusicPlayer player={this.state} toggleMusic={this.toggleMusic} />
</React.Fragment>
)} />
</div>
</div>
</Router>
);
}
}
export default App;
Uncaught TypeError: Cannot read property 'setState' of undefined
The error occurs because of how the this keyword works in JavaScript. I think the Audio should play just fine if we solve that issue.
If you do a console.log(this) inside play() you will see that this it is undefined and that's why it throws that error, since you are doing this.setState().Basically the value of this inside play() depends upon how that function is invoked.
There are two common solutions with React:
Using bind() to set the value of a function's this regardless of how it's called:
constructor(props) {
super(props);
this.play() = this.play.bind(this);
}
Using arrow functions which don't provide their own this binding
<button onClick={() => {this.play()}}>Play</button>
Now you will have access to this.setState and this.audio inside play(), and the same goes for pause().
You can try this, it work on me
var tinung = `${window.location.origin}/terimakasih.ogg`;
var audio = document.createElement("audio");
audio.autoplay = true;
audio.load();
audio.addEventListener(
"load",
function() {
audio.play();
},
true
);
audio.src = tinung;
How to prevent a user from tapping a button twice in React native?
i.e. A user must not be able tap twice quickly on a touchable highlight
https://snack.expo.io/#patwoz/withpreventdoubleclick
Use this HOC to extend the touchable components like TouchableHighlight, Button ...
import debounce from 'lodash.debounce'; // 4.0.8
const withPreventDoubleClick = (WrappedComponent) => {
class PreventDoubleClick extends React.PureComponent {
debouncedOnPress = () => {
this.props.onPress && this.props.onPress();
}
onPress = debounce(this.debouncedOnPress, 300, { leading: true, trailing: false });
render() {
return <WrappedComponent {...this.props} onPress={this.onPress} />;
}
}
PreventDoubleClick.displayName = `withPreventDoubleClick(${WrappedComponent.displayName ||WrappedComponent.name})`
return PreventDoubleClick;
}
Usage
import { Button } from 'react-native';
import withPreventDoubleClick from './withPreventDoubleClick';
const ButtonEx = withPreventDoubleClick(Button);
<ButtonEx onPress={this.onButtonClick} title="Click here" />
Use property Button.disabled
import React, { Component } from 'react';
import { AppRegistry, StyleSheet, View, Button } from 'react-native';
export default class App extends Component {
state={
disabled:false,
}
pressButton() {
this.setState({
disabled: true,
});
// enable after 5 second
setTimeout(()=>{
this.setState({
disabled: false,
});
}, 5000)
}
render() {
return (
<Button
onPress={() => this.pressButton()}
title="Learn More"
color="#841584"
disabled={this.state.disabled}
accessibilityLabel="Learn more about this purple button"
/>
);
}
}
// skip this line if using Create React Native App
AppRegistry.registerComponent('AwesomeProject', () => App);
Here is my simple hook.
import { useRef } from 'react';
const BOUNCE_RATE = 2000;
export const useDebounce = () => {
const busy = useRef(false);
const debounce = async (callback: Function) => {
setTimeout(() => {
busy.current = false;
}, BOUNCE_RATE);
if (!busy.current) {
busy.current = true;
callback();
}
};
return { debounce };
};
This can be used anywhere you like. Even if it's not for buttons.
const { debounce } = useDebounce();
<Button onPress={() => debounce(onPressReload)}>
Tap Me again and adain!
</Button>
Agree with Accepted answer but very simple way , we can use following way
import debounce from 'lodash/debounce';
componentDidMount() {
this.onPressMethod= debounce(this.onPressMethod.bind(this), 500);
}
onPressMethod=()=> {
//what you actually want on button press
}
render() {
return (
<Button
onPress={() => this.onPressMethod()}
title="Your Button Name"
/>
);
}
I use it by refer the answer above. 'disabled' doesn't have to be a state.
import React, { Component } from 'react';
import { TouchableHighlight } from 'react-native';
class PreventDoubleTap extends Component {
disabled = false;
onPress = (...args) => {
if(this.disabled) return;
this.disabled = true;
setTimeout(()=>{
this.disabled = false;
}, 500);
this.props.onPress && this.props.onPress(...args);
}
}
export class ButtonHighLight extends PreventDoubleTap {
render() {
return (
<TouchableHighlight
{...this.props}
onPress={this.onPress}
underlayColor="#f7f7f7"
/>
);
}
}
It can be other touchable component like TouchableOpacity.
If you are using react navigation then use this format to navigate to another page.
this.props.navigation.navigate({key:"any",routeName:"YourRoute",params:{param1:value,param2:value}})
The StackNavigator would prevent routes having same keys to be pushed in the stack again.
You could write anything unique as the key and the params prop is optional if you want to pass parameters to another screen.
The accepted solution works great, but it makes it mandatory to wrap your whole component and to import lodash to achieve the desired behavior.
I wrote a custom React hook that makes it possible to only wrap your callback:
useTimeBlockedCallback.js
import { useRef } from 'react'
export default (callback, timeBlocked = 1000) => {
const isBlockedRef = useRef(false)
const unblockTimeout = useRef(false)
return (...callbackArgs) => {
if (!isBlockedRef.current) {
callback(...callbackArgs)
}
clearTimeout(unblockTimeout.current)
unblockTimeout.current = setTimeout(() => isBlockedRef.current = false, timeBlocked)
isBlockedRef.current = true
}
}
Usage:
yourComponent.js
import React from 'react'
import { View, Text } from 'react-native'
import useTimeBlockedCallback from '../hooks/useTimeBlockedCallback'
export default () => {
const callbackWithNoArgs = useTimeBlockedCallback(() => {
console.log('Do stuff here, like opening a new scene for instance.')
})
const callbackWithArgs = useTimeBlockedCallback((text) => {
console.log(text + ' will be logged once every 1000ms tops')
})
return (
<View>
<Text onPress={callbackWithNoArgs}>Touch me without double tap</Text>
<Text onPress={() => callbackWithArgs('Hello world')}>Log hello world</Text>
</View>
)
}
The callback is blocked for 1000ms after being called by default, but you can change that with the hook's second parameter.
I have a very simple solution using runAfterInteractions:
_GoCategoria(_categoria,_tipo){
if (loading === false){
loading = true;
this.props.navigation.navigate("Categoria", {categoria: _categoria, tipo: _tipo});
}
InteractionManager.runAfterInteractions(() => {
loading = false;
});
};
Did not use disable feature, setTimeout, or installed extra stuff.
This way code is executed without delays. I did not avoid double taps but I assured code to run just once.
I used the returned object from TouchableOpacity described in the docs https://reactnative.dev/docs/pressevent and a state variable to manage timestamps. lastTime is a state variable initialized at 0.
const [lastTime, setLastTime] = useState(0);
...
<TouchableOpacity onPress={async (obj) =>{
try{
console.log('Last time: ', obj.nativeEvent.timestamp);
if ((obj.nativeEvent.timestamp-lastTime)>1500){
console.log('First time: ',obj.nativeEvent.timestamp);
setLastTime(obj.nativeEvent.timestamp);
//your code
SplashScreen.show();
await dispatch(getDetails(item.device));
await dispatch(getTravels(item.device));
navigation.navigate("Tab");
//end of code
}
else{
return;
}
}catch(e){
console.log(e);
}
}}>
I am using an async function to handle dispatches that are actually fetching data, in the end I'm basically navigating to other screen.
Im printing out first and last time between touches. I choose there to exist at least 1500 ms of difference between them, and avoid any parasite double tap.
You can also show a loading gif whilst you await some async operation. Just make sure to tag your onPress with async () => {} so it can be await'd.
import React from 'react';
import {View, Button, ActivityIndicator} from 'react-native';
class Btn extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false
}
}
async setIsLoading(isLoading) {
const p = new Promise((resolve) => {
this.setState({isLoading}, resolve);
});
return p;
}
render() {
const {onPress, ...p} = this.props;
if (this.state.isLoading) {
return <View style={{marginTop: 2, marginBottom: 2}}>
<ActivityIndicator
size="large"
/>
</View>;
}
return <Button
{...p}
onPress={async () => {
await this.setIsLoading(true);
await onPress();
await this.setIsLoading(false);
}}
/>
}
}
export default Btn;
My implementation of wrapper component.
import React, { useState, useEffect } from 'react';
import { TouchableHighlight } from 'react-native';
export default ButtonOneTap = ({ onPress, disabled, children, ...props }) => {
const [isDisabled, toggleDisable] = useState(disabled);
const [timerId, setTimerId] = useState(null);
useEffect(() => {
toggleDisable(disabled);
},[disabled]);
useEffect(() => {
return () => {
toggleDisable(disabled);
clearTimeout(timerId);
}
})
const handleOnPress = () => {
toggleDisable(true);
onPress();
setTimerId(setTimeout(() => {
toggleDisable(false)
}, 1000))
}
return (
<TouchableHighlight onPress={handleOnPress} {...props} disabled={isDisabled} >
{children}
</TouchableHighlight>
)
}