From e35bed21e3c36c324fff5d2f604ca8480ffa85bd Mon Sep 17 00:00:00 2001 From: Nuwan Date: Thu, 5 Mar 2026 19:37:34 +0530 Subject: [PATCH] feat(32-03): create JKResyncButton with colocated loading state - Extracted resync button from JKSessionScreen - Local loading state prevents parent re-renders - memo() wrapper for render optimization - Preserves original error handling logic --- .../src/components/client/JKResyncButton.js | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 jam-ui/src/components/client/JKResyncButton.js diff --git a/jam-ui/src/components/client/JKResyncButton.js b/jam-ui/src/components/client/JKResyncButton.js new file mode 100644 index 000000000..3840d6ca5 --- /dev/null +++ b/jam-ui/src/components/client/JKResyncButton.js @@ -0,0 +1,60 @@ +import React, { useState, useCallback, memo } from 'react'; +import { Button, Spinner } from 'reactstrap'; +import { toast } from 'react-toastify'; +import PropTypes from 'prop-types'; + +/** + * Self-contained resync button with colocated loading state. + * Loading state changes only re-render this component, not the parent. + * + * State colocation: https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster + */ +const JKResyncButton = memo(({ resyncAudio, className }) => { + const [loading, setLoading] = useState(false); + + const handleClick = useCallback(async (e) => { + e.preventDefault(); + if (loading) return; + + setLoading(true); + try { + await resyncAudio(); + // Silent success (matches legacy behavior) + } catch (error) { + if (error.message === 'timeout') { + toast.error('Audio resync timed out. Please try again.'); + } else { + toast.error('Audio resync failed: ' + (error.message || 'Unknown error')); + } + } finally { + setLoading(false); + } + }, [resyncAudio, loading]); + + return ( + + ); +}); + +JKResyncButton.displayName = 'JKResyncButton'; + +JKResyncButton.propTypes = { + resyncAudio: PropTypes.func.isRequired, + className: PropTypes.string +}; + +export default JKResyncButton;