Skip to main content

useVersion

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

Signature

const useVersion: (callback?: Fn) => readonly [number, () => void]

Parameters

NameTypeDescription
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

Examples

Example 1

// 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>
);
};

Playground

// 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>
);
};