@winglet/react-utils
Version:
React utility library providing custom hooks, higher-order components (HOCs), and utility functions to enhance React application development with improved reusability and functionality
251 lines (250 loc) • 8.25 kB
TypeScript
import type { Fn } from '../@aileron/declare';
/**
* Provides a manual re-render trigger with optional side effects and render counting.
*
* This hook creates a simple but powerful mechanism for forcing component updates
* by incrementing an internal counter. Each call to the update function increments
* the counter and triggers a re-render, optionally executing a callback first.
* The counter value serves as a "render generation" indicator.
*
* ### Primary Use Cases
* - **Force Re-renders**: Update component when external data changes outside React's tracking
* - **Cache Invalidation**: Invalidate memoized values by changing dependency
* - **Manual Refresh**: Implement refresh buttons, pull-to-refresh, or manual sync
* - **External State Sync**: Re-render after non-React state changes (global stores, DOM events)
* - **Debug Re-renders**: Track and count component update cycles
* - **Key Prop Generation**: Force remount child components with changing keys
*
* ### When React Doesn't Know About Changes
* Sometimes you need to force re-renders due to changes React can't automatically detect:
* - External library state changes
* - DOM manipulations outside React
* - Global variables or window properties
* - WebSocket/Server-sent events
* - LocalStorage/SessionStorage changes
*
* @example
* ```typescript
* // Basic manual refresh functionality
* const DataList = () => {
* const [renderCount, refresh] = useVersion();
* const [data, setData] = useState([]);
*
* const fetchData = async () => {
* const response = await api.getData();
* setData(response);
* };
*
* useEffect(() => {
* fetchData();
* }, [renderCount]); // Refetch when refresh is triggered
*
* return (
* <div>
* <div>Render #{renderCount}</div>
* <button onClick={refresh}>Refresh Data</button>
* <DataDisplay data={data} />
* </div>
* );
* };
*
* // Force re-render with side effects
* const ExternalDataSync = ({ externalStore }) => {
* const [version, syncData] = useVersion(() => {
* console.log('Syncing with external data source...');
* externalStore.refresh();
* analytics.track('ManualSync', { timestamp: Date.now() });
* });
*
* useEffect(() => {
* // Listen to external store changes
* const unsubscribe = externalStore.onChange(() => {
* syncData(); // Force re-render when external data changes
* });
* return unsubscribe;
* }, [syncData]);
*
* return (
* <div>
* <div>Sync version: {version}</div>
* <div>Data: {externalStore.getCurrentData()}</div>
* <button onClick={syncData}>Manual Sync</button>
* </div>
* );
* };
*
* // Cache invalidation pattern
* const ExpensiveCalculation = ({ input }) => {
* const [cacheVersion, invalidateCache] = useVersion();
*
* const expensiveResult = useMemo(() => {
* console.log('Recalculating expensive result...');
* return performExpensiveCalculation(input);
* }, [input, cacheVersion]); // Cache invalidated when version changes
*
* return (
* <div>
* <div>Result: {expensiveResult}</div>
* <div>Cache version: {cacheVersion}</div>
* <button onClick={invalidateCache}>Force Recalculate</button>
* </div>
* );
* };
*
* // Child component remounting
* const FormWithReset = ({ initialData }) => {
* const [resetKey, resetForm] = useVersion();
* // Key prop forces complete remount of form
* return (
* <div>
* <ComplexForm key={resetKey} initialData={initialData} />
* <button onClick={resetForm}>Reset Form</button>
* </div>
* );
* };
*
* // External library integration
* const ThirdPartyChart = ({ data }) => {
* const [version, forceUpdate] = useVersion();
* const chartRef = useRef(null);
* const chartInstanceRef = useRef(null);
*
* useEffect(() => {
* if (chartRef.current) {
* // Initialize third-party chart
* chartInstanceRef.current = new ExternalChart(chartRef.current, {
* data,
* onDataChange: () => forceUpdate() // Force re-render on external changes
* });
* }
*
* return () => {
* chartInstanceRef.current?.destroy();
* };
* }, [data, forceUpdate, version]); // Include version to trigger effect
*
* return (
* <div>
* <div ref={chartRef} />
* <div>Chart render: {version}</div>
* <button onClick={forceUpdate}>Refresh Chart</button>
* </div>
* );
* };
*
* // Performance monitoring and debugging
* const PerformanceMonitor = ({ children }) => {
* const [renderCount, triggerRender] = useVersion();
* const lastRenderTime = useRef(Date.now());
* const renderTimes = useRef<number[]>([]);
*
* useEffect(() => {
* const now = Date.now();
* const timeSinceLastRender = now - lastRenderTime.current;
* renderTimes.current.push(timeSinceLastRender);
* lastRenderTime.current = now;
*
* // Keep only last 10 render times
* if (renderTimes.current.length > 10) {
* renderTimes.current = renderTimes.current.slice(-10);
* }
*
* console.log(`Render #${renderCount}, Time since last: ${timeSinceLastRender}ms`);
* });
*
* const averageRenderTime = renderTimes.current.length > 0
* ? renderTimes.current.reduce((a, b) => a + b, 0) / renderTimes.current.length
* : 0;
*
* return (
* <div>
* <div className="debug-info">
* <span>Renders: {renderCount}</span>
* <span>Avg time: {averageRenderTime.toFixed(1)}ms</span>
* <button onClick={triggerRender}>Force Render</button>
* </div>
* {children}
* </div>
* );
* };
*
* // Real-time data with manual refresh
* const LiveDashboard = () => {
* const [version, refresh] = useVersion(() => {
* console.log('Dashboard refresh triggered');
* // Could trigger analytics, notifications, etc.
* });
*
* const [data, setData] = useState(null);
* const [lastUpdate, setLastUpdate] = useState(null);
*
* useEffect(() => {
* const fetchLiveData = async () => {
* const response = await fetch('/api/live-data');
* const newData = await response.json();
* setData(newData);
* setLastUpdate(new Date().toLocaleString());
* };
*
* fetchLiveData();
*
* // Auto-refresh every 30 seconds
* const interval = setInterval(fetchLiveData, 30000);
* return () => clearInterval(interval);
* }, [version]); // Manual refresh also triggers data fetch
*
* return (
* <div>
* <header>
* <h1>Live Dashboard (v{version})</h1>
* <div>Last updated: {lastUpdate}</div>
* <button onClick={refresh}>Refresh Now</button>
* </header>
* <DashboardContent data={data} />
* </div>
* );
* };
*
* // Error recovery with version reset
* const ErrorRecoveryComponent = () => {
* const [version, resetComponent] = useVersion(() => {
* console.log('Component reset triggered');
* // Clear error states, reset caches, etc.
* });
*
* const [error, setError] = useState(null);
*
* const handleError = (error: Error) => {
* setError(error);
* console.error('Component error:', error);
* };
*
* const handleReset = () => {
* setError(null);
* resetComponent(); // Trigger fresh render cycle
* };
*
* if (error) {
* return (
* <div className="error-boundary">
* <h2>Something went wrong (Render #{version})</h2>
* <pre>{error.message}</pre>
* <button onClick={handleReset}>Reset Component</button>
* </div>
* );
* }
*
* return (
* <ErrorBoundary onError={handleError}>
* <ComplexComponent key={version} />
* </ErrorBoundary>
* );
* };
* ```
*
* @param callback - Optional function to execute before incrementing the version and triggering re-render
* @returns A tuple of [version, updateVersion]:
* - version: Current version number (starts at 0, increments with each update)
* - updateVersion: Function to increment version and trigger re-render
*/
export declare const useVersion: (callback?: Fn) => readonly [number, () => void];