עדכון סטייט בתוך עדכון סטייט
בוובינר היום ביקשנו מקלוד לעדכן קומפוננטת ריאקט שמציגה קרוסלת תמונות ולהוסיף לה שעון עצר שמראה כמה זמן עד החלפת התמונה הבאה. בהתחלה הוא יצר שני אפקטים וכשהתעקשנו ליצור אפקט אחד קיבלנו את הקוד הבא:
useEffect(() => {
if (images.length <= 1) return
// Reset countdown when component mounts or when interval changes
setCountdown(interval / 1000)
const timer = setInterval(() => {
setCountdown((prevCountdown) => {
if (prevCountdown <= 1) {
// Time to switch image
setCurrentIndex((prevIndex) => (prevIndex + 1) % images.length)
return interval / 1000 // Reset countdown
}
return prevCountdown - 1
})
}, 1000) // Update every second
return () => clearInterval(timer)
}, [images.length, interval])
שימו לב לקריאה ל setCurrentIndex בתוך פונקציית העדכון שעוברת ל setCountdown. יצא לי לכתוב קוד בסגנון הזה וכל פעם שזה קרה זה העיד על איזושהי בעיה בתכנון הקומפוננטה ובסופו של דבר יצא משליטה וגרם לבאגים מוזרים במקומות אחרים. ובכל זאת הקוד של קלוד עובד.
אחרי הוובינר ניסיתי להבין מה הפריע לי בפיתרון ואני חושב שהבעיה היא כפילות בסטייט - בעצם יש פה שני משתני סטייט קשורים ואנחנו צריכים את העדכון בתוך עדכון כדי לבטא את הקשר הזה. אפשר לחשוב על זה שרצוי שמשתני סטייט של קומפוננטה יהיו אורתוגונליים וכשיש קשר ביניהם זה אומר שמשהו לא מדויק בארכיטקטורה.
אם יש משתני סטייט קשורים אולי זה אומר שאחד מהם יכול להיות ערך מחושב? כיוון אחד יכול להיות לספור רק את הזמן שעבר מאז שהקומפוננטה נכנסה למסך, ואז אפשר לחשב את אינדקס התמונה להצגה עם חילוק ושארית. הבעיה היא מה עושים עם הלחיצות. אם ממשיכים באותו כיוון אפשר להגדיר שהלחיצות "יקדמו" את השעון, ואז יש לנו את הקוד הבא:
import { useState, useEffect } from 'react'
import './ImageCarousel.css'
interface ImageCarouselProps {
images: string[]
interval?: number
}
const ImageCarousel: React.FC<ImageCarouselProps> = ({ images, interval = 3000 }) => {
const [elapsedTime, setElapsedTime] = useState(0)
// Derive currentIndex and countdown from elapsedTime
const currentCycle = Math.floor(elapsedTime / interval)
const currentIndex = currentCycle % images.length
const timeInCurrentCycle = elapsedTime % interval
const countdown = Math.ceil((interval - timeInCurrentCycle) / 1000)
useEffect(() => {
if (images.length <= 1) return
const timer = setInterval(() => {
setElapsedTime((prevTime) => prevTime + 1000)
}, 1000) // Update every second
return () => clearInterval(timer)
}, [images.length, interval])
const goToSlide = (index: number) => {
// Calculate the elapsed time that would result in the desired index
const targetCycle = Math.floor(elapsedTime / interval) + (index - currentIndex)
const adjustedCycle = targetCycle >= 0 ? targetCycle : targetCycle + Math.ceil(Math.abs(targetCycle) / images.length) * images.length
setElapsedTime(adjustedCycle * interval)
}
const goToPrevious = () => {
const prevIndex = currentIndex === 0 ? images.length - 1 : currentIndex - 1
goToSlide(prevIndex)
}
const goToNext = () => {
const nextIndex = (currentIndex + 1) % images.length
goToSlide(nextIndex)
}
if (images.length === 0) {
return <div className="carousel-empty">No images to display</div>
}
return (
<div className="carousel-container">
<div className="carousel-wrapper">
<button
className="carousel-btn carousel-btn-prev"
onClick={goToPrevious}
aria-label="Previous image"
>
‹
</button>
<div className="carousel-image-container">
<img
src={images[currentIndex]}
alt={`Slide ${currentIndex + 1}`}
className="carousel-image"
/>
</div>
<button
className="carousel-btn carousel-btn-next"
onClick={goToNext}
aria-label="Next image"
>
›
</button>
</div>
<div className="carousel-indicators">
{images.map((_, index) => (
<button
key={index}
className={`carousel-indicator ${index === currentIndex ? 'active' : ''}`}
onClick={() => goToSlide(index)}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
{images.length > 1 && (
<div className="carousel-countdown">
Next image in: {countdown}s
</div>
)}
</div>
)
}
export default ImageCarousel
גם הוא עובד הפעם עם משתנה סטייט יחיד.
כיוון אחר לשבור את הקשר הוא לפרק את הקומפוננטה לשתי קומפוננטות - קומפוננטה אחת של קרוסלת תמונות וקומפוננטה שניה היא השעון עצר. זה הקוד עם הפרדת קומפוננטות:
import { useState, useEffect } from 'react'
import './ImageCarousel.css'
interface CountdownTimerProps {
interval: number
onTimeout: () => void
}
const CountdownTimer: React.FC<CountdownTimerProps> = ({ interval, onTimeout }) => {
const [countdown, setCountdown] = useState(interval / 1000)
useEffect(() => {
const timer = setInterval(() => {
setCountdown((prevCountdown) => {
if (prevCountdown <= 1) {
onTimeout()
return interval / 1000 // Reset countdown
}
return prevCountdown - 1
})
}, 1000) // Update every second
return () => clearInterval(timer)
}, [interval, onTimeout])
return (
<div className="carousel-countdown">
Next image in: {countdown}s
</div>
)
}
interface ImageCarouselProps {
images: string[]
interval?: number
}
const ImageCarousel: React.FC<ImageCarouselProps> = ({ images, interval = 3000 }) => {
const [currentIndex, setCurrentIndex] = useState(0)
const goToSlide = (index: number) => {
setCurrentIndex(index)
}
const goToPrevious = () => {
setCurrentIndex((prevIndex) =>
prevIndex === 0 ? images.length - 1 : prevIndex - 1
)
}
const goToNext = () => {
setCurrentIndex((prevIndex) => (prevIndex + 1) % images.length)
}
if (images.length === 0) {
return <div className="carousel-empty">No images to display</div>
}
return (
<div className="carousel-container">
<div className="carousel-wrapper">
<button
className="carousel-btn carousel-btn-prev"
onClick={goToPrevious}
aria-label="Previous image"
>
‹
</button>
<div className="carousel-image-container">
<img
src={images[currentIndex]}
alt={`Slide ${currentIndex + 1}`}
className="carousel-image"
/>
</div>
<button
className="carousel-btn carousel-btn-next"
onClick={goToNext}
aria-label="Next image"
>
›
</button>
</div>
<div className="carousel-indicators">
{images.map((_, index) => (
<button
key={index}
className={`carousel-indicator ${index === currentIndex ? 'active' : ''}`}
onClick={() => goToSlide(index)}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
{images.length > 1 && (
<CountdownTimer
key={currentIndex}
interval={interval}
onTimeout={goToNext}
/>
)}
</div>
)
}
export default ImageCarousel
כאן שוב יש לנו את ה set בתוך set, אבל הפעם הזזתי את האפקט רק לקומפוננטה של השעון שהיא הרבה יותר קטנה וכל משתנה סטייט שמור בקומפוננטה שלו.
מה דעתכם? איזה גירסה אתם הכי אוהבים ולמה?