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
This commit is contained in:
Nuwan 2026-03-05 19:37:34 +05:30
parent f0ddd9d7c7
commit e35bed21e3
1 changed files with 60 additions and 0 deletions

View File

@ -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 (
<Button
className={className || 'btn-custom-outline'}
outline
size="md"
onClick={handleClick}
disabled={loading}
>
{loading ? (
<>
<Spinner size="sm" /> Resyncing...
</>
) : (
'Resync'
)}
</Button>
);
});
JKResyncButton.displayName = 'JKResyncButton';
JKResyncButton.propTypes = {
resyncAudio: PropTypes.func.isRequired,
className: PropTypes.string
};
export default JKResyncButton;