Improve performance - Avoid Re-renders when changing range input used as a volume slider

How would i go about improving this. Right now when you move the slider it'll re-renders all the Quotes Im having a hard time wrapping my head around this. My thinking is I would need to manage the sound with useSound somewhere else? or do some type of composition with the components? If i do the former, then you won't be able to play multiple sounds simultaneously? Idk Any advice would be appreciated thanks.
3 Replies
Řambo
Řambo15mo ago
export default function Home({
championData,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const [query, setQuery] = useState('');
const [visibleItems, setVisibleItems] = useState(90);
const queriedData = useFuzzySearch(
query,
['quote', 'name'],
championData,
visibleItems
);
const containerRef = useRef<HTMLDivElement>(null);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
containerRef.current?.scrollTo({ top: 0, behavior: 'instant' });
const newVisibleItems = visibleItems === 90 ? visibleItems : 90;
setVisibleItems(newVisibleItems);
};

return (
<VolumeProvider>
<section
ref={containerRef}
className="flex flex-col gap-4 max-h-screen overflow-y-auto no-scrollbar"
>
<VolumeSlider />
<input
className={}
type="text"
value={query}
placeholder="Search quotes..."
onChange={handleChange}
/>
<InfiniteScroller
loadMore={() => {
setVisibleItems((prevVisibleItems) => prevVisibleItems + 50);
}}
loader={<p>Loading...</p>}
rootMargin="200px"
hasMore={queriedData.length >= visibleItems}
>
<List
items={queriedData.slice(0, visibleItems)}
keyExtractor={({ name, quote, url }) => name + quote + url}
className="grid gap-4 auto-rows-fr sm:grid-cols-2 px-4 xl:grid-cols-3"
renderItem={({ name, icon, quote, url, skin }, i) => (
<Quote
quote={quote}
champName={name}
champIcon={icon}
quoteAudioURL={url}
skin={skin}
/>
)}
/>
</InfiniteScroller>
</section>
</VolumeProvider>
);
}
export default function Home({
championData,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const [query, setQuery] = useState('');
const [visibleItems, setVisibleItems] = useState(90);
const queriedData = useFuzzySearch(
query,
['quote', 'name'],
championData,
visibleItems
);
const containerRef = useRef<HTMLDivElement>(null);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
containerRef.current?.scrollTo({ top: 0, behavior: 'instant' });
const newVisibleItems = visibleItems === 90 ? visibleItems : 90;
setVisibleItems(newVisibleItems);
};

return (
<VolumeProvider>
<section
ref={containerRef}
className="flex flex-col gap-4 max-h-screen overflow-y-auto no-scrollbar"
>
<VolumeSlider />
<input
className={}
type="text"
value={query}
placeholder="Search quotes..."
onChange={handleChange}
/>
<InfiniteScroller
loadMore={() => {
setVisibleItems((prevVisibleItems) => prevVisibleItems + 50);
}}
loader={<p>Loading...</p>}
rootMargin="200px"
hasMore={queriedData.length >= visibleItems}
>
<List
items={queriedData.slice(0, visibleItems)}
keyExtractor={({ name, quote, url }) => name + quote + url}
className="grid gap-4 auto-rows-fr sm:grid-cols-2 px-4 xl:grid-cols-3"
renderItem={({ name, icon, quote, url, skin }, i) => (
<Quote
quote={quote}
champName={name}
champIcon={icon}
quoteAudioURL={url}
skin={skin}
/>
)}
/>
</InfiniteScroller>
</section>
</VolumeProvider>
);
}
function Quote({ quote, champName, quoteAudioURL, skin }: Props) {
const { isPlaying, play, pause } = useSound();
return (
<div className="relative h-full rounded-md overflow-hidden z-0 flex items-center p-4">
<Image
src={skin}
alt="Champion Skin"
fill
className="object-cover -z-10"
/>
<div className="absolute inset-0 bg-black bg-opacity-60" />

<div className="z-10 flex items-center gap-4">
{isPlaying ? (
<PauseCircle
size={56}
color="white"
className="flex-shrink-0 cursor-pointer"
onClick={pause}
/>
) : (
<PlayCircle
size={56}
color="white"
className="flex-shrink-0 cursor-pointer"
onClick={() => play(quoteAudioURL)}
/>
)}
<div>
<p
className={`${spiegel.variable} line-clamp-3 text-xl font-spiegel`}
>
{quote}
</p>
<p
className={`${spiegel.variable} text-gray-400 font-spiegel font-semibold tracking-wide`}
>
{champName}
</p>
</div>
</div>
</div>
);
}
function Quote({ quote, champName, quoteAudioURL, skin }: Props) {
const { isPlaying, play, pause } = useSound();
return (
<div className="relative h-full rounded-md overflow-hidden z-0 flex items-center p-4">
<Image
src={skin}
alt="Champion Skin"
fill
className="object-cover -z-10"
/>
<div className="absolute inset-0 bg-black bg-opacity-60" />

<div className="z-10 flex items-center gap-4">
{isPlaying ? (
<PauseCircle
size={56}
color="white"
className="flex-shrink-0 cursor-pointer"
onClick={pause}
/>
) : (
<PlayCircle
size={56}
color="white"
className="flex-shrink-0 cursor-pointer"
onClick={() => play(quoteAudioURL)}
/>
)}
<div>
<p
className={`${spiegel.variable} line-clamp-3 text-xl font-spiegel`}
>
{quote}
</p>
<p
className={`${spiegel.variable} text-gray-400 font-spiegel font-semibold tracking-wide`}
>
{champName}
</p>
</div>
</div>
</div>
);
}
import { useVolume } from '@/VolumeContext';
import { ElementRef, useEffect, useState } from 'react';

function useSound() {
const [audio, setAudio] = useState<ElementRef<'audio'> | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const { volume } = useVolume();

useEffect(() => {
setAudio(new Audio());
}, []);

useEffect(() => {
if (!audio) return;

const handleEnded = () => {
setIsPlaying(false);
};

audio.addEventListener('ended', handleEnded);

return () => {
if (audio) {
audio.removeEventListener('ended', handleEnded);
}
};
}, [audio]);

useEffect(() => {
if (!audio) return;
console.log('USESOUND VOLUME CHANGE');
audio.volume = volume;
}, [audio, volume]);

const play = (src: string) => {
if (!audio) return;
audio.src = src;
audio.play();
setIsPlaying(true);
};

const pause = () => {
if (!audio) return;
audio.pause();
setIsPlaying(false);
};

return { play, pause, isPlaying };
}

export default useSound;
import { useVolume } from '@/VolumeContext';
import { ElementRef, useEffect, useState } from 'react';

function useSound() {
const [audio, setAudio] = useState<ElementRef<'audio'> | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const { volume } = useVolume();

useEffect(() => {
setAudio(new Audio());
}, []);

useEffect(() => {
if (!audio) return;

const handleEnded = () => {
setIsPlaying(false);
};

audio.addEventListener('ended', handleEnded);

return () => {
if (audio) {
audio.removeEventListener('ended', handleEnded);
}
};
}, [audio]);

useEffect(() => {
if (!audio) return;
console.log('USESOUND VOLUME CHANGE');
audio.volume = volume;
}, [audio, volume]);

const play = (src: string) => {
if (!audio) return;
audio.src = src;
audio.play();
setIsPlaying(true);
};

const pause = () => {
if (!audio) return;
audio.pause();
setIsPlaying(false);
};

return { play, pause, isPlaying };
}

export default useSound;
import { ReactNode, createContext, useContext, useState } from 'react';

type VolumeContextData = {
volume: number;
updateVolume: (newVolume: number) => void;
};

const VolumeContext = createContext<VolumeContextData>({
volume: 0.5,
updateVolume: () => {},
});

export function useVolume() {
return useContext(VolumeContext);
}

type VolumeProviderProps = {
children: ReactNode;
};

export function VolumeProvider({ children }: VolumeProviderProps) {
const [volume, setVolume] = useState(0.5);

const updateVolume = (newVolume: number) => {
setVolume(newVolume);
};

return (
<VolumeContext.Provider value={{ volume, updateVolume }}>
{children}
</VolumeContext.Provider>
);
}
import { ReactNode, createContext, useContext, useState } from 'react';

type VolumeContextData = {
volume: number;
updateVolume: (newVolume: number) => void;
};

const VolumeContext = createContext<VolumeContextData>({
volume: 0.5,
updateVolume: () => {},
});

export function useVolume() {
return useContext(VolumeContext);
}

type VolumeProviderProps = {
children: ReactNode;
};

export function VolumeProvider({ children }: VolumeProviderProps) {
const [volume, setVolume] = useState(0.5);

const updateVolume = (newVolume: number) => {
setVolume(newVolume);
};

return (
<VolumeContext.Provider value={{ volume, updateVolume }}>
{children}
</VolumeContext.Provider>
);
}
import { useVolume } from '@/VolumeContext';
import React, { useCallback } from 'react';

const VolumeSlider = () => {
const { volume, updateVolume } = useVolume();

const handleVolumeChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const newVolume = parseFloat(event.target.value);
updateVolume(newVolume);
},
[updateVolume]
);

return (
<input
type="range"
min={0}
max={1}
step={0.01}
value={volume}
onChange={handleVolumeChange}
className="accent-gold"
/>
);
};

export default VolumeSlider;
import { useVolume } from '@/VolumeContext';
import React, { useCallback } from 'react';

const VolumeSlider = () => {
const { volume, updateVolume } = useVolume();

const handleVolumeChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const newVolume = parseFloat(event.target.value);
updateVolume(newVolume);
},
[updateVolume]
);

return (
<input
type="range"
min={0}
max={1}
step={0.01}
value={volume}
onChange={handleVolumeChange}
className="accent-gold"
/>
);
};

export default VolumeSlider;
Jon Higger (He / Him)
you could debounce or throttle it, or use a state manager less "Pass values in everywhere to achieve reactivity" such as zustand or something like that.
Řambo
Řambo15mo ago
alright thanks
Want results from more Discord servers?
Add your server