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
Quote
s
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
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;
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.
alright thanks