I'm trying to have react forms save and then update some user settings.
import React, { useState, useEffect } from "react";
import Dropdown from "components/Settings/Preferences/Dropdown";
import { useForm } from "react-hook-form";
function Pill({ value, handleChange,optionName,substanceKey,key }) {
return (
<div className="bg-gray-600 text-white text-xs px-2 py-0.5 w-max-content rounded-lg align-middle mr-1 mb-1">
{value}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="inline-block align-middle cursor-pointer"
onClick={() => handleChange(value, "remove",optionName,substanceKey)}
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</div>
);
}
function InventoryPage(props) {
const [substanceDetails, setSettings] = useState(
[
{
substance: "Modafinil",
scheduledtimes: [8, 13],
startdate: "1996-12-02",
enddate: "2020-01-01",
duration: 2,
planneddose: "10mg"
},
{
substance: "Coveve",
scheduledtimes: [8, 12],
startdate: "1996-12-02",
enddate: "2020-01-01",
duration: 2,
planneddose: "10mg"
}
);
const { register, handleSubmit,watch, errors,formState } = useForm();
const handleChange = (value, mode,optionName,substanceKey) => {
var removedSubstances ;
if(mode==="remove") {
if (optionName === "substance" ) {
removedSubstances = substanceDetails.find(v=> v.substance === value) ? substanceDetails.filter(v => v.substance !== value) : [...substanceDetails, value]
}
else {
removedSubstances = substanceDetails.reduce((acc, key) => {
// return acc; // remove keys
if (optionName === "scheduledtimes") { //works
// remove 14 in times for keys
return [...acc, { ...key,
scheduledtimes: key.scheduledtimes.filter(time => time !== value)
}]
}
if (optionName === "planneddose") {
// modify the power by concatenating an new object with partial info
if (key.substance == substanceKey){
return [...acc, {...key,
planneddose: null
}];
} else {
return [...acc, {...key,
planneddose: key.planneddose
}];
}
}
if (optionName === "startdate") {
// modify the power by concatenating an new object with partial info
if (key.substance == substanceKey){
return [...acc, {...key,
startdate: null
}];
} else {
return [...acc, {...key,
startdate: key.startdate
}];
}
}
if (optionName === "enddate") {
// modify the power by concatenating an new object with partial info
if (key.substance == substanceKey){
return [...acc, {...key,
enddate: null
}];
} else {
return [...acc, {...key,
enddate: key.enddate
}];
}
}
if (optionName === "duration") {
// modify the power by concatenating an new object with partial info
if (key.substance == substanceKey){
return [...acc, {...key,
duration: null
}];
} else {
return [...acc, {...key,
duration: key.duration
}];
}
}
}
, []);
}
setSettings(removedSubstances)
}
};
const onSubmit = data => console.log(data);
const [pending, setPending] = useState(false);
console.log(watch("example")); // watch input value by passing the name of it
if (substanceDetails === false) {
return (
<div className="md:grid md:grid-cols-3 md:gap-6">
<div className="md:col-span-1">
<h3 className="text-lg font-medium leading-6 text-gray-900">
Substances
</h3>
</div>
<div className="mt-5 md:mt-0 md:col-span-2 font-mono font-medium text-blue-500">
loading...
</div>
</div>
);
}
return (
<div className="md:grid md:grid-cols-3 md:gap-6">
<div className="mt-5 md:mt-0 md:col-span-2">
<form onSubmit={handleSubmit(onSubmit)} id="formName">
<div className="flex flex-wrap mt-2">
{substanceDetails &&
substanceDetails.map((subst) => (
<Pill
registerInput={register}
optionLabel="substance"
value={subst.substance}
key={subst.substance}
substanceKey = {subst.substance}
optionName={"substance"}
// allOptions={["Dexamphetamine", "Ritalin"]}
handleChange={handleChange}
error={formState.errors?.content ? true : false}
/>
))}
</div>
<Dropdown
registerInput={register}
optionLabel="Substance"
selectedOption={substanceDetails.substance}
optionName={"substance"}
allOptions={["Dexamphetamine", "Ritalin"]}
error={formState.errors?.content ? true : false}
/>
<button
className="inline-flex items-center justify-center px-5 py-2 border border-transparent text-base leading-6 font-medium rounded-md text-indigo-700 bg-indigo-100 hover:text-indigo-600 hover:bg-indigo-50 focus:outline-none focus:shadow-outline focus:border-indigo-300 transition duration-150 ease-in-out"
variant={props.buttonColor}
size={props.inputSize}
type="submit"
disabled={pending}
form="formName"
>
{pending ? (
<>
<span>Saving</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="animate-spin h-4 w-4 ml-3 fill-current"
>
<path d="M0 11c.511-6.158 5.685-11 12-11s11.489 4.842 12 11h-2.009c-.506-5.046-4.793-9-9.991-9s-9.485 3.954-9.991 9h-2.009zm21.991 2c-.506 5.046-4.793 9-9.991 9s-9.485-3.954-9.991-9h-2.009c.511 6.158 5.685 11 12 11s11.489-4.842 12-11h-2.009z" />
</svg>
</>
) : (
<span>Save</span>
)}
</button>
</form>
</div>
</div>
);
}
export default InventoryPage;
Currently when hitting submit, the form itself is just refreshing the page, and it's not using the onSubmit constant. I'm not sure what's going on, love some help :)
Sandbox link, code is compiling but probably easier to vet the code itself
https://codesandbox.io/s/using-tailwind-with-nextjs-forked-nnr0l?file=/pages/index.js
You are calling onSubmit inside handleSubmitRemove.
Removing onSubmit from handleSubmitRemove might work.
Try using the id attribute on the form tag, i.e
<form onSubmit={handleSubmit(onSubmit)} id="formName">
And on the button tag, you use the form attribute which will be the same as the form id, i.e
<button
variant={props.buttonColor}
size={props.inputSize}
type="submit"
disabled={pending}
form="formName"
>
instead of button try input element with type="submit" and style it
if nothing works, from above suggestions.
use preventDefault for submit
and handle submit with a different function , get all the data you need using document.getElement or querySelector and handle them in the function.
Please take a look at this example
https://codesandbox.io/s/using-tailwind-with-next-js-forked-tizjq?file=/pages/index.js
After required fields are filled submit button is clicked, form submits
Your CodeSandbox link wouldn't compile for me, but take a look at this discussion on the RHF github about how to prevent form submission in the submit handler. It's involving async stuff but should be similar for your needs. I think you just need to pass the event and preventDefault as others have said, and maybe their example will be worth following.
Related
*is it possible to integrate in my ready checkout paypal code to add monthly payment function?
I have developed a function that is responsible for receiving a one-time payment, but I need to add a function that will be responsible for charging a monthly fixed amount from a person who wants to make a monetary contribution
*
import React, { useEffect } from "react";
import {
PayPalScriptProvider,
PayPalButtons,
usePayPalScriptReducer,
} from "#paypal/react-paypal-js";
import imgLogo from "../../../images/FUMlogo.png";
import imgColageDesk from "../../../images/Donate/colageDesk.png";
import DonateBTN from "../../UI/Button/DonateBTN/DonateBTN";
import DonateOther from "../../UI/Button/DonateBTN/DonateOther";
import { useDispatch, useSelector } from "react-redux";
import {
setAmountMoney,
setFlowAmountMoney,
} from "../../../Store/Reducers/inputMoneyAmount";
export default function Donate() {
const amountDonate = useSelector((state) => state.inputMoneyAmount.amount);
const flowAmountDonate = useSelector(
(state) => state.inputMoneyAmount.flowAmount
);
const dispatch = useDispatch();
const amount50 = () => {
dispatch(setFlowAmountMoney(false));
dispatch(setAmountMoney(50));
};
const amount100 = () => {
dispatch(setFlowAmountMoney(false));
dispatch(setAmountMoney(100));
};
const amount200 = () => {
dispatch(setFlowAmountMoney(false));
dispatch(setAmountMoney(200));
};
// This values are the props in the UI
const amount = amountDonate ? amountDonate : flowAmountDonate + "";
const currency = "USD";
const style = { layout: "vertical", color: "blue" };
// Custom component to wrap the PayPalButtons and handle currency changes
const ButtonWrapper = ({ currency, showSpinner }) => {
// usePayPalScriptReducer can be use only inside children of PayPalScriptProviders
// This is the main reason to wrap the PayPalButtons in a new component
const [{ options, isPending }, dispatch] = usePayPalScriptReducer();
useEffect(() => {
dispatch({
type: "resetOptions",
value: {
...options,
currency: currency,
},
});
}, [currency, showSpinner]);
return (
<>
{showSpinner && isPending && <div className="spinner" />}
<PayPalButtons
style={style}
disabled={false}
forceReRender={[amount, currency, style]}
fundingSource={undefined}
createOrder={(data, actions) => {
return actions.order
.create({
purchase_units: [
{
amount: {
currency_code: currency,
value: amount,
},
},
],
})
.then((orderId) => {
// Your code here after create the order
return orderId;
});
}}
onApprove={function (data, actions) {
return actions.order.capture().then(function () {
// Your code here after capture the order
});
}}
/>
</>
);
};
// bg-[#010321]
return (
<main className="flex flex-col items-center w-full h-screen sm:h-auto text-white bg-[#1b1d33] pt-[8vh] sm:pt-[9vw] lg:pt-[7vw] xl:pt-[5vw] relative ">
<section className=" flex flex-col items-center justify-center px-[3vw] ">
<img
className=" w-full sm:w-[70vw] md:w-[60vw] lg:w-[40vw] xl:w-[30vw]"
src={imgColageDesk}
alt="colage"
/>
<img
className="w-[10vw] lg:w-[8vw] xl:w-[5vw] h-[10vw] lg:h-[8vw] xl:h-[5vw] mt-10"
src={imgLogo}
alt="logo"
/>
<h3 className="text-xs sm:text-sm md:text-base lg:text-lg mt-5">
Donate to
</h3>
<h1 className="uppercase mt-3 text-sm sm:text-base md:text-lg lg:text-xl font-bold ">
forward ukraine ministries
</h1>
<p className="w-full sm:w-[70vw] md:w-[60vw] lg:w-[40vw] xl:w-[30vw] text-center text-xs sm:text-sm md:text-base lg:text-lg mt-2">
A humanitarian catastrophe is unfolding in Ukraine as attacks of war
put millions of lives at risk. Support Military & Refugee
</p>
<div className="w-[80vw] sm:w-[50vw] md:w-[30vw] flex justify-around mt-14 mb-4 sm:mt-15 text-black">
<DonateBTN setAmount={amount50} amount="$50" />
<DonateBTN setAmount={amount100} amount="$100" />
<DonateBTN setAmount={amount200} amount="$200" />
<DonateOther name="Other" />
</div>
<input
value={flowAmountDonate}
onChange={(e) => dispatch(setFlowAmountMoney(e.target.value))}
onKeyDown={(e) =>
["ArrowUp", "ArrowDown", "e", "E", "+", "-", ".", ","].includes(
e.key
) && e.preventDefault()
}
className={`${
flowAmountDonate
? "block font-bold w-[80%] sm:w-3/4 md:w-[85%] h-[7vw] sm:h-[6vw] md:h-[4vw] mb-4 sm:mb-8 text-black rounded-lg bg-gradient-to-r from-blue-500 to-yellow-500 focus:from-pink-500 focus:to-yellow-500 hover:from-pink-500 hover:to-yellow-500"
: "hidden"
}`}
type="number"
name=""
/>
<PayPalScriptProvider
options={{
"client-id": "test",
components: "buttons",
currency: "USD",
}}
>
<ButtonWrapper currency={currency} showSpinner={false} />
</PayPalScriptProvider>
</section>
</main>
);
}
Recurring payments use a separate process.
For react-paypal-js in particular you can see an example of a Subscription checkout here. You can create subscription plans in your live PayPal account here (or for a sandbox business account here, or via API here). The client-id that a subscription plan is created for must correspond to the client-id that the JS SDK is loaded with for checkout. If it does not correspond, the error will be RESOURCE_NOT_FOUND.
I have a table in my database and I am trying to calculate the total in my react application.
The problem is that when I use a filter I get back not a number (NaN)
I need to show the total price when applying the different filters.
What I tried was to define a function like this:
const [totalRevenue, setTotalRevenue] = useState(0);
useEffect(() => {
console.log("songs", songs);
const total = songs.reduce((acc, song) => {
const n = Number(song.Revenue);
return isNaN(n) ? acc : acc + n;
}, 0);
setTotalRevenue(total);
}, [songs]);
The filter looks something like this:
const displaySongs = songs
.slice(pagesVisited, pagesVisited + songsPerPage)
.filter((song) => {
let keepSong = true;
keepSong &&=
filterDistributor === "" || totalRevenue === "" ||
song.Distributor?.toLowerCase().includes(
filterDistributor.toLowerCase() || song.Distributor?.Revenue?.toLowerCase().includes(totalRevenue.toLowerCase())
);
return keepSong;
})
.map((song) => {
return (
<tr className="font-medium bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
<td className="py-2 px-4 text-left">{song.Distributor}</td>
<td className="py-2 px-4 text-left">{song.Revenue}</td>
</tr>
);
});
return (
<div>
<div className="flex w-full">
<select
id="Labels"
className="bg-slate-900 text-white border border-gray-500 text-sm focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
value={filterDistributor || totalRevenue}
onChange={(e) => setFilterDistributor(e.target.value) || setTotalRevenue(e.target.value)}
>
<option value="">Choose a Distributor</option>
{[...new Set(songs.map((song) => song.Distributor))].map(
(Distributor) => (
<option key={Distributor} value={Distributor}>
{Distributor}
</option>
)
)}
</select>
</div>
<div className="w-96 p-6 my-10 gap-4 shadow-md rounded-md bg-slate-900 text-center">
<p className="font-normal text-white dark:text-gray-400">
Total Revenue:
</p>
<p className="text-2xl font-bold tracking-tight text-white dark:text-white">
{Math.round(totalRevenue).toFixed(2)} €
</p>
</div>
</div>
Unfortunately, as mentioned above, this function returns an invalid value.
Here is an example of my data which is the same as you get when console logging:
[
{
"id": 1,
"Period From": "2021-01-31T23:00:00.000Z",
"Period To": "2021-02-27T23:00:00.000Z",
"Transaction Date": null,
"Distributor": "DistriburName",
"UPC": "UpcNumber",
"Cat. No.": "CaNumber",
"ISRC": "CaNumber",
"Label": "LabelName",
"Release Title": "ReleaseTitle",
"Track Title": "TrackTitle",
"Mix Name": "Original Mix",
"Artist": "ArtistName",
"Content Type": "Asset",
"Delivery Method": "Non-Interactive Stream",
"Territory": "US",
"Quantity": 2,
"Revenue": 0.001091
}
]
Its because you are using the reduce function wrong.
here is something you can try:
useEffect(() => {
console.log("songs", songs);
const total = songs.reduce(function(prev, cur) {
if(isNaN(cur.Revenue)){
console.log("is not a number")
return prev
}
return prev + cur.Revenue
}, 0);
setTotalRevenue(total);
}, [songs]);
reference: https://ibb.co/4R73VSj
check the updated answer, just so u know, NaN is a number and when u convert any string to Number you cannot check if its a number or not after because it will always be a number, cause NaN is a number try doing this
console.log(isNaN(NaN))
it will return true
I am using React Query to make API calls.
I have an OTP Generation API in which I am making a POST API call to generate an OTP as a response from the API I receive the status of OTP deliverance.
/* eslint-disable no-unused-vars */
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { TextField } from '../Input/TextField';
import { CustomButton } from '../Button/CustomButton';
import { MOBILE_NUMBER } from '../Common/Placeholder';
import { getOtpData } from '../../hooks/getOtp.hook';
export function MobileNumber() {
const navigate = useNavigate();
const [mobileno, setMobileNo] = useState('');
const [isTermsAgree, setisTermsAgree] = useState(false);
const [isDisplayLoader, setDisplayLoader] = useState(false);
const [isDisplayError, setDisplayError] = useState(false);
const { mutate, isError, isSuccess, isLoading, isIdle, data } =
getOtpData();
// Onchnage event for input mobile number
const handleNumberChange = (
e: React.ChangeEvent<HTMLInputElement>,
) => {
setMobileNo(e.target.value);
};
// Onchnage event for Checkbox
const TermsAgreeChange = () => {
setisTermsAgree((current) => !current);
};
// onClick Event Confirm Btn //Generate OTP API call Goes Here
const getOtp = () => {
mutate(mobileno);
if (isSuccess) {
if (data?.data.otpSent) {
console.log('Sent - true');
navigate('/phone-otp-confirmation', {
state: { phoneNumber: mobileno },
});
}
if (data?.data.maxOtpRetriesExceeded) {
setDisplayError(true);
}
}
if (isError) {
console.log('error');
}
};
return (
<div className="bg-grey-800 h-1/2 mt-40 flex flex-col justify-evenly font-Manrope ">
<section>
<div className=" flex-col flex items-center md:items-baseline md:pl-36 ">
<p className=" text-3xl "> Enter Mobile Number </p>
</div>
<div>
<p className="text-l flex-col flex items-center mt-1 md:items-baseline md:pl-36 ">
<span className=" text-gray-400 text-center ">
Enter Mobile Number used for instant login
</span>
</p>
</div>
<div className="flex-col flex items-center md:items-baseline md:pl-36 mt-5">
<div className=" flex items-center sm:justify-start sm:px-0 ">
<div>
<div className=" flex w-18 px-3 justify-center items-center bg-transparent rounded-bl-lg rounded-tl-lg border text-2xl md:text-3xl border-gray-700 h-12 md:h-16 focus:outline-none focus:bg-transparent">
<span>+91</span>
</div>
</div>
<div>
<TextField
width="w-48"
height="h-12"
padding="px-5"
placeholder={MOBILE_NUMBER}
maxlen={10}
other="rounded-br-lg rounded-tr-lg px-5 md:w-72 md:h-16"
type="text"
onChangeFunction={handleNumberChange}
val={mobileno}
error={false}
/>
</div>
</div>
</div>
</section>
<div className=" flex-col flex mt-16 items-center md:items-baseline md:pl-36 md:mt-5 ">
<div className="flex items-center w-72">
<TextField
width="w-7"
height="h-7"
type="checkbox"
other="form-checkbox"
onChangeFunction={TermsAgreeChange}
/>
<p className="ml-3 text-sm md:text-base tracking-wide text-gray-400 font-extralight">
I have read the OneCard{' '}
<a
href="http://"
className="underline text-sm md:text-base text-gray-400"
>
Terms and Conditions & Privacy Policy
</a>{' '}
</p>
</div>
<div className="mt-8 ">
<CustomButton
clickEvent={getOtp}
btntext="Get OTP"
isbuttonactive={mobileno.length === 10 && isTermsAgree}
/>
</div>
{/* <h2>Loader</h2>
<h2>Error</h2> */}
</div>
</div>
);
}
OTP Generation hook
import { useMutation } from 'react-query';
import axios from 'axios';
import { WEB } from '../constants/constants';
interface IGetOTPResult {
otpSent: boolean;
maxOtpRetriesExceeded: boolean;
}
const getOTP = async (mobileNumber: string) => {
const response = await axios.post<IGetOTPResult>(
`${process.env.REACT_APP_URL}/`,
{
mobile: mobileNumber
},
{
headers: {
Authorization: '',
'Content-Type': 'application/json',
},
},
);
return response;
};
export const getOtpData = () => {
return useMutation(getOTP);
};
PROBLEM : As soon as I make this API call through the frontend as I click the button, it goes into isIdle state to be true.
Only the second time, I click the button, isSuccess becomes true.
However, bot the times the API call is made and I receive a 200 response!
I want to ensure my API call never enters isIdle state.
Plus, there is no significant information given about isIDle in any of react-queries documentation.
How do I go about this?
This is not how state in react works. when you call mutate, react-query updates state in your component, and on the next render cycle, it will be available. It is the same concept as setState, you can't really do:
function MyComponent() {
const [foo, setFoo] = React.useState('foo')
return <button onClick={() => {
setFoo('something')
console.log(foo) // 🚨 this will still log "foo", not "something"
}}>click</button>
}
if you want to get access directly to the response, you have to either:
use the provided callbacks of mutate:
mutate(
mobileno,
{
onSuccess: (response) => {
// handle success here
},
onError: (error) => {
// handle error here
}
)
use mutateAsync and await:
try {
const response = await mutateAsync(mobileno)
// handle success here
} catch(error) {
// handle error here
}
side question: how can a hook be called getOtpData ? It has to start with use...
I have an array of drawer items that I'm trying to toggle the state of. I wrote a function that I thought would work in toggling the individual drawer items but that isn't working. I'm getting a TypeError: drawer.setState is not a function. Not exactly sure how to get that working. Does anyone have any tips on how to do that? Thanks in advance!
class CategoryList extends React.Component<CategoryListProps, CategoryListState> {
constructor(props) {
super(props);
const drawers = this.props.items.map((item) => ({
open: false,
heading: item.heading,
drawerHeading: item.drawerHeading,
drawerContent: item.drawerContent,
}));
this.state = { drawers };
this.handleToggle = this.handleToggle.bind(this);
}
handleToggle = (drawer) => {
drawer.setState({ open: !drawer.open });
};
render() {
return (
<div className="pb-10">
<h5 className="pb-8 text-white md:text-center">{this.props.heading}</h5>
<div className="">
{this.state.drawers.map((drawer) => (
<div className="flex flex-col items-baseline w-full py-6 text-white border-b md:flex-row border-b-white">
<h3 className="flex-shrink-0">{drawer.heading}</h3>
<div className="right">
<p className="font-serif text-xl">{drawer.drawerHeading}</p>
{drawer.open && <CategoryListDrawer drawer={drawer} />}
</div>
<div className={`flex items-center self-end justify-center w-12 h-12 ml-auto transform border border-white rounded-full toggle-btn cursor-pointer ${drawer.open === true ? 'open' : ''}`} onClick={this.handleToggle(drawer)}>
<span className="w-6 h-6 overflow-hidden transition-transform duration-75 transform toggle"></span>
</div>
</div>
))}
</div>
</div>
);
}
}
drawer is an object that looks like this:
{
open: false,
heading: item.heading,
drawerHeading: item.drawerHeading,
drawerContent: item.drawerContent,
}
It doesn't have a setState function. To set state you'll need to do this.setState. But you also need to modify your code in a few other ways to make it work.
First, you will be updating the entire array, so your code needs to find the item in the array and update just that. Might be easier of you pass an index instead of the drawer, but we can make it work with the drawer:
handleToggle = (drawer) {
this.setState(prev => {
const index = prev.drawers.indexOf(drawer);
const newDrawers = [...prev.drawers];
newDrawers[index] = {
...newDrawers[index],
open: !newDrawers[index].open
}
return { drawers: newDrawers }
});
}
Second, you're currently calling handleToggle immediately, during rendering, and then passing undefined into onClick. Instead, you need to create a new function and pass that into onClick. Ie, instead of this:
onClick={this.handleToggle(drawer)}
Do this:
onClick={() => this.handleToggle(drawer)}
Third, there's no use in binding an arrow function, so you can delete this line:
this.handleToggle = this.handleToggle.bind(this);
I am trying to use Howler.js in Reactjs using typescript.
I can able to play the sound but it does not pause or stop. Here is my code.
This a component where I am passing all the audio details using props.
I did console.log() to check, is it going in else part and it goes and print the console.
Please help me in this
import React, { useState } from 'react';
import Button from 'components/button/button';
import PlayIcon from 'assets/icons/play.svg';
import PauseIcon from 'assets/icons/pause.svg';
import AudioWave from 'assets/icons/sound-wave.svg';
import { Howl, Howler } from 'howler';
interface Props {
name?: string,
audio?: any,
loop?: boolean,
autoplay?: boolean
}
const Audio = (props: Props) => {
const { name, audio, loop, autoplay } = props;
const [isPlaying, setIsPlaying] = useState(false);
const [audioId, setAudioId] = useState(0);
const sound = new Howl({
src: [audio],
autoplay: autoplay,
loop: loop,
volume: 1,
onend: function () {
setIsPlaying(false);
},
});
Howler.volume(0.5);
const playAudio = () => {
let Id: any;
if (!isPlaying) {
Id = sound.play();
setAudioId(Id);
setIsPlaying(true);
console.log('THS')
} else {
sound.stop(audioId);
console.log('THATAT', audioId)
}
console.log(sound)
}
return (
<div className="flex flex-col items-center justify-center rounded shadow-md w-full">
{console.log(isPlaying, audioId)}
<div className="grid grid-cols-12 w-full">
<div className="col-span-6 p-2">
<p className="text-left">
{name}
</p>
</div>
<div className="col-span-6 p-2">
<p className="text-right text-light-gray">
{sound ? `${Duration.toTimeFormat(sound._duration * 1000)}s` : '0:00s'}
</p>
</div>
</div>
<div className="grid grid-cols-12 w-full items-center justify-center">
<div className="grid col-span-2 w-full p-2">
<img
className="w-full cursor"
onClick={() => playAudio()}
src={isPlaying ? PauseIcon : PlayIcon}
alt="PlayPauseIcon"
/>
</div>
<div className="grid col-span-10 p-2">
<img className="w-full" alt="Audio Wave" src={AudioWave} />
</div>
</div>
</div>
)
}
export default Audio;
I'm not too familiar with Howler, but it seems it's not 'react-friendly' - more suited for a non-SPA environment. However, I figure if you try it like this, you might get the results you're looking for. The main issue with your code is that every time you change something via useState, the component will re-render. When it re-renders, any variables that are not stored via useState, will have their values re-initialized. Therefore, in your code from the question, the sound variable gets reset on every render making it impossible to control the element. In the code below I shifted that into a useEffect function, which is similar to componentDidMount. It will only run on the first render; thus, it prevents you from having several copies of the sound object.
I haven't tested this code, but I think in general it targets the main issue you're having.
const Audio = (props: Props) => {
const { name, audio, loop, autoplay } = props;
const [isPlaying, setIsPlaying] = useState(false);
const [audioId, setAudioId] = useState(0);
const [sound, setSound] = useState(null);
useEffect(() => {
const s = new Howl({
src: [audio],
autoplay: autoplay,
loop: loop,
volume: 0.5,
onplay: function (id) {
setAudioId(id);
setIsPlaying(true);
},
onpause: function (id) {
setIsPlaying(false);
},
onend: function () {
setIsPlaying(false);
},
onloaderror: function(id, error) {
console.log(`a load error has occured on id ${id}`);
console.error(error);
},
onplayerror: function(id, error) {
console.log(`a play error has occured on id ${id}`);
console.error(error);
}
});
setSound(s);
});
const toggleAudioState = () => {
if (sound !== null) {
if (!isPlaying) {
sound.play();
setIsPlaying(true);
} else {
sound.stop(audioId);
}
}
}
return (
<div className="flex flex-col items-center justify-center rounded shadow-md w-full">
<div className="grid grid-cols-12 w-full">
<div className="col-span-6 p-2">
<p className="text-left">
{name}
</p>
</div>
<div className="col-span-6 p-2">
<p className="text-right text-light-gray">
{sound ? `${Duration.toTimeFormat(sound._duration * 1000)}s` : '0:00s'}
</p>
</div>
</div>
<div className="grid grid-cols-12 w-full items-center justify-center">
<div className="grid col-span-2 w-full p-2">
<img
className="w-full cursor"
onClick={() => toggleAudioState()}
src={isPlaying ? PauseIcon : PlayIcon}
alt="PlayPauseIcon"
/>
</div>
<div className="grid col-span-10 p-2">
<img className="w-full" alt="Audio Wave" src={AudioWave} />
</div>
</div>
</div>
)
}
I got the solution for that : -
Here it should be define global. But we can't do it in react, because we are receiving all the data through props.
const sound = new Howl({ } );
If we define this able the function then it will work -
const sound = new Howl({
src: ['some_music_link.mp3'],
autoplay: true,
loop: true,
volume: 1,
});
const Audio = (props :Props) => {
...
}
In order to solve this problem someone created a new npm package for that called Router Howler
So, if you are trying to use howler.js in react then use React Howler