From c3870b30f35e990a947528d7d628e1082c18f060 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Mon, 16 Dec 2024 19:21:40 +0000 Subject: [PATCH] refactor(slider): enhance performance and readability with memoization and style organization --- .../Form/Settings/Slider/Slider.jsx | 322 +++++++++++------- 1 file changed, 201 insertions(+), 121 deletions(-) diff --git a/src/components/Form/Settings/Slider/Slider.jsx b/src/components/Form/Settings/Slider/Slider.jsx index 41eadd4e..1b5a7ee3 100644 --- a/src/components/Form/Settings/Slider/Slider.jsx +++ b/src/components/Form/Settings/Slider/Slider.jsx @@ -1,156 +1,236 @@ import variables from 'config/variables'; -import { useState } from 'react'; +import { useState, useCallback, memo, useMemo } from 'react'; import { toast } from 'react-toastify'; import ReactSlider from 'react-slider'; import { MdRefresh } from 'react-icons/md'; import EventBus from 'utils/eventbus'; +import clsx from 'clsx'; + +// Style definitions split into logical groups for better readability +const buttonStyles = { + base: 'inline-flex items-center text-sm px-3 py-1.5 rounded-md font-medium', + colors: 'bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-400', + darkMode: 'dark:bg-white/10 dark:hover:bg-white/15 dark:active:bg-white/20', + text: 'text-neutral-800 dark:text-white', + transitions: 'transition-colors duration-200', +}; + +const thumbStyles = { + base: [ + 'w-5 h-5 rounded-full cursor-pointer', + 'absolute top-1/2 transform -translate-y-1/2', + 'flex items-center justify-center', + ], + colors: 'bg-gradient-to-br from-neutral-50 to-neutral-200 dark:from-white dark:to-neutral-100', + interactions: [ + 'hover:from-neutral-100 hover:to-neutral-300', + 'dark:hover:from-white dark:hover:to-neutral-200', + 'focus:outline-none focus:ring-2 focus:ring-neutral-400 dark:focus:ring-white/40', + 'active:scale-95', + ], + effects: 'shadow-lg shadow-black/10 dark:shadow-black/25 transition-all duration-200', + tooltip: [ + 'before:content-[attr(aria-valuenow)]', + 'before:absolute before:top-[-28px]', + 'before:text-xs before:bg-neutral-800 dark:before:bg-black/90', + 'before:text-white before:px-2 before:py-1 before:rounded-md', + 'before:opacity-0 hover:before:opacity-100', + 'before:transition-all before:duration-200 before:whitespace-nowrap', + 'before:shadow-lg', + ], +}; + +const markStyles = { + base: [ + 'h-3 w-1.5 rounded-full cursor-pointer select-none', + 'absolute top-1/2 transform -translate-y-1/2', + ], + colors: 'bg-neutral-400 dark:bg-white/30', + hover: 'hover:bg-neutral-600 dark:hover:bg-white/50 hover:scale-110', + transitions: 'hover:transition-transform hover:duration-200', + label: [ + 'after:content-[attr(data-value)]', + 'after:absolute after:top-5', + 'after:text-xs after:opacity-85', + 'after:transform after:-translate-x-1/2', + 'after:whitespace-nowrap after:pointer-events-none', + 'after:font-medium', + 'after:text-neutral-700 dark:after:text-white/85', + ], +}; + +const MULTIPLIER_MARKS = { + 10: '0.1x', + 100: '1x', + 200: '2x', + 400: '4x', +}; + +// Memoized value display component +const ValueDisplay = memo(({ value, display }) => ( + + {value} + {display} + +)); + +// Memoized reset button component +const ResetButton = memo(({ onClick }) => ( + +)); function SliderComponent(props) { - const [value, setValue] = useState( - Number(localStorage.getItem(props.name)) || Number(props.default), + const [value, setValue] = useState(() => { + try { + const stored = localStorage.getItem(props.name); + return stored ? Number(stored) : Number(props.default); + } catch (e) { + return Number(props.default); + } + }); + + const handleChange = useCallback( + (newValue) => { + if (typeof newValue !== 'number') return; + const clampedValue = Math.min(Math.max(newValue, props.min), props.max); + + // Update state immediately for smooth UI + setValue(clampedValue); + + // Batch heavy operations in requestAnimationFrame + requestAnimationFrame(() => { + try { + localStorage.setItem(props.name, clampedValue); + + if (props.element && !document.querySelector(props.element)) { + const reminderElement = document.querySelector('.reminder-info'); + if (reminderElement) { + reminderElement.style.display = 'flex'; + localStorage.setItem('showReminder', true); + return; + } + } + + EventBus.emit('refresh', props.category); + } catch (e) { + console.error('Error updating slider:', e); + } + }); + }, + [props.min, props.max, props.name, props.element, props.category], ); - const handleChange = (newValue) => { - if (typeof newValue !== 'number') return; - newValue = Math.min(Math.max(newValue, props.min), props.max); - localStorage.setItem(props.name, newValue); - setValue(newValue); - - if (props.element) { - if (!document.querySelector(props.element)) { - document.querySelector('.reminder-info').style.display = 'flex'; - return localStorage.setItem('showReminder', true); - } - } - - EventBus.emit('refresh', props.category); - }; - - const resetItem = () => { + const resetItem = useCallback(() => { handleChange(Number(props.default)); toast(variables.getMessage('toasts.reset')); - }; + }, [props.default, handleChange]); - const multiplierMarks = { - 10: '0.1x', - 100: '1x', - 200: '2x', - 400: '4x', - }; + const handleMarkClick = useCallback( + (markValue) => { + handleChange(markValue); + }, + [handleChange], + ); - const handleMarkClick = (markValue) => { - handleChange(markValue); - }; + // Memoized render functions to prevent unnecessary re-renders + const renderThumb = useMemo( + () => (thumbProps, state) => ( +
+ ), + [props.display], + ); + + const renderMark = useMemo( + () => (markProps) => { + if (!MULTIPLIER_MARKS[markProps.key]) return null; + + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + handleMarkClick(markProps.key); + }} + onMouseDown={(e) => e.preventDefault()} + data-value={MULTIPLIER_MARKS[markProps.key]} + role="button" + tabIndex={0} + aria-label={`Set zoom to ${MULTIPLIER_MARKS[markProps.key]}`} + className={`${markProps.className} cursor-pointer`} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleMarkClick(markProps.key); + } + }} + /> + ); + }, + [handleMarkClick], + ); return (
{props.title}
- - {value} - {props.display} - - + +
+ ( -
- )} - renderMark={(props) => { - if (multiplierMarks[props.key]) { - return ( -
{ - e.preventDefault(); - e.stopPropagation(); - handleMarkClick(props.key); - }} - onMouseDown={(e) => e.preventDefault()} - data-value={multiplierMarks[props.key]} - role="button" - tabIndex={0} - aria-label={`Set zoom to ${multiplierMarks[props.key]}`} - className={`${props.className} cursor-pointer`} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleMarkClick(props.key); - } - }} - /> - ); - } - return null; - }} + markClassName={clsx([ + ...markStyles.base, + markStyles.colors, + markStyles.hover, + markStyles.transitions, + ...markStyles.label, + ])} + renderThumb={renderThumb} + renderMark={renderMark} />
); } -export { SliderComponent as default, SliderComponent as Slider }; +// Export memoized version for better performance when parent components re-render +export const Slider = memo(SliderComponent); +export default Slider;