@utilityjs/use-get-latest
Version:
A React hook that stores & updates `ref.current` with the most recent value.
385 lines (297 loc) • 10.2 kB
Markdown
<div align="center">
A React hook that stores & updates `ref.current` with the most recent value.
</div>
<hr />
- **Stale Closure Prevention**: Avoid stale closures in callbacks and async
operations
- **Stable References**: Create stable callback references without exhaustive
dependencies
- **SSR Compatible**: Uses isomorphic layout effect for server-side rendering
compatibility
- **TypeScript Support**: Full type safety with generic support
- **Performance Optimized**: Minimal re-renders by avoiding dependency arrays
- **Simple API**: Easy to use with any value type
```bash
npm install @utilityjs/use-get-latest
```
or
```bash
pnpm add @utilityjs/use-get-latest
```
```tsx
import { useGetLatest } from "@utilityjs/use-get-latest";
import { useState, useCallback } from "react";
function SearchComponent({ onSearch, query }) {
const latestOnSearch = useGetLatest(onSearch);
const latestQuery = useGetLatest(query);
const handleSearch = useCallback(async () => {
// These values are always fresh, even if props change
// after this callback was created
const results = await searchAPI(latestQuery.current);
latestOnSearch.current(results);
}, []); // No dependencies needed!
return <button onClick={handleSearch}>Search for: {query}</button>;
}
```
```tsx
import { useGetLatest } from "@utilityjs/use-get-latest";
import { useState, useEffect } from "react";
function Counter({ step, isRunning }) {
const [count, setCount] = useState(0);
const latestStep = useGetLatest(step);
const latestIsRunning = useGetLatest(isRunning);
useEffect(() => {
const interval = setInterval(() => {
if (latestIsRunning.current) {
setCount(c => c + latestStep.current);
}
}, 1000);
return () => clearInterval(interval);
}, []); // No need to restart interval when step or isRunning changes
return (
<div>
<p>Count: {count}</p>
<p>Step: {step}</p>
<p>Running: {isRunning ? "Yes" : "No"}</p>
</div>
);
}
```
```tsx
import { useGetLatest } from "@utilityjs/use-get-latest";
import { useState, useCallback } from "react";
function FormComponent({ onSubmit, validationRules }) {
const [formData, setFormData] = useState({});
const latestFormData = useGetLatest(formData);
const latestValidationRules = useGetLatest(validationRules);
const latestOnSubmit = useGetLatest(onSubmit);
const handleSubmit = useCallback(async e => {
e.preventDefault();
// Always use the latest values
const isValid = validateForm(
latestFormData.current,
latestValidationRules.current,
);
if (isValid) {
await latestOnSubmit.current(latestFormData.current);
}
}, []); // Stable callback reference
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit">Submit</button>
</form>
);
}
```
```tsx
import { useGetLatest } from "@utilityjs/use-get-latest";
import { useEffect } from "react";
function WebSocketComponent({ onMessage, onError, url }) {
const latestOnMessage = useGetLatest(onMessage);
const latestOnError = useGetLatest(onError);
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = event => {
// Always calls the latest onMessage handler
latestOnMessage.current(JSON.parse(event.data));
};
ws.onerror = error => {
// Always calls the latest onError handler
latestOnError.current(error);
};
return () => ws.close();
}, [url]); // Only reconnect when URL changes
return <div>WebSocket connection active</div>;
}
```
```tsx
import { useGetLatest } from "@utilityjs/use-get-latest";
import { useState, useCallback, useRef } from "react";
function SearchInput({ onSearch, debounceMs = 300 }) {
const [query, setQuery] = useState("");
const latestQuery = useGetLatest(query);
const latestOnSearch = useGetLatest(onSearch);
const timeoutRef = useRef<NodeJS.Timeout>();
const debouncedSearch = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
// Uses the latest query and onSearch function
latestOnSearch.current(latestQuery.current);
}, debounceMs);
}, [debounceMs]); // Only recreate when debounce time changes
const handleInputChange = e => {
setQuery(e.target.value);
debouncedSearch();
};
return (
<input
type="text"
value={query}
onChange={handleInputChange}
placeholder="Search..."
/>
);
}
```
```tsx
import { useGetLatest } from "@utilityjs/use-get-latest";
import { useState, useEffect } from "react";
function AnimatedComponent({ targetValue, duration, onComplete }) {
const [currentValue, setCurrentValue] = useState(0);
const latestTargetValue = useGetLatest(targetValue);
const latestOnComplete = useGetLatest(onComplete);
useEffect(() => {
let animationId: number;
const startTime = Date.now();
const startValue = currentValue;
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
// Always animate towards the latest target value
const target = latestTargetValue.current;
const newValue = startValue + (target - startValue) * progress;
setCurrentValue(newValue);
if (progress < 1) {
animationId = requestAnimationFrame(animate);
} else {
// Call the latest completion handler
latestOnComplete.current?.(target);
}
};
animationId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationId);
}, [duration]); // Only restart animation when duration changes
return (
<div>
Current: {currentValue.toFixed(2)} / Target: {targetValue}
</div>
);
}
```
```tsx
import { useGetLatest } from "@utilityjs/use-get-latest";
import { useEffect, useState } from "react";
function useAsyncData<T>(
fetchFn: () => Promise<T>,
dependencies: any[],
onSuccess?: (data: T) => void,
onError?: (error: Error) => void,
) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const latestFetchFn = useGetLatest(fetchFn);
const latestOnSuccess = useGetLatest(onSuccess);
const latestOnError = useGetLatest(onError);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
// Always use the latest fetch function
const result = await latestFetchFn.current();
if (!cancelled) {
setData(result);
latestOnSuccess.current?.(result);
}
} catch (err) {
if (!cancelled) {
const error = err as Error;
setError(error);
latestOnError.current?.(error);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, dependencies);
return { data, loading, error };
}
```
- `value: T` - The value to store and keep updated in the ref
- `RefObject<T>` - A ref object with a `.current` property containing the latest
value
- `T` - The type of the value being stored
1. **Initial Value**: The ref is initialized with the provided value
2. **Updates**: The ref is updated with the latest value on every render using
`useLayoutEffect`
3. **SSR Safe**: Uses `useEffect` on the server and `useLayoutEffect` on the
client
4. **Synchronous**: Updates happen synchronously during the render phase
(client-side)
#### Use Cases
- **Stale Closures**: Prevent stale closures in event handlers, intervals, and
async operations
- **Stable Callbacks**: Create stable callback references without exhaustive
dependency arrays
- **Latest Values**: Access the most recent value in long-running operations
- **Performance**: Avoid unnecessary re-renders by reducing dependency arrays
#### Best Practices
1. **Use for Callbacks**: Ideal for event handlers and callback functions that
need latest values
2. **Async Operations**: Perfect for accessing latest state in promises,
intervals, and timeouts
3. **Stable References**: Use to create stable function references without
dependencies
4. **Don't Overuse**: Only use when you specifically need to avoid stale
closures
5. **Combine with useCallback**: Often used together with `useCallback` for
stable, latest-value callbacks
#### Common Patterns
```typescript
// Pattern 1: Stable callback with latest values
const latestValue = useGetLatest(value);
const stableCallback = useCallback(() => {
doSomething(latestValue.current);
}, []); // No dependencies needed
// Pattern 2: Async operation with latest state
const latestState = useGetLatest(state);
useEffect(() => {
const asyncOperation = async () => {
const result = await api.call();
// latestState.current always has the latest value
updateUI(latestState.current, result);
};
asyncOperation();
}, [trigger]); // Only depend on trigger, not state
// Pattern 3: Event handler with latest props
const latestProps = useGetLatest(props);
const handleEvent = useCallback(event => {
processEvent(event, latestProps.current);
}, []); // Stable reference
```
Read the
[](https://github.com/mimshins/utilityjs/blob/main/CONTRIBUTING.md)
to learn about our development process, how to propose bug fixes and
improvements, and how to build and test your changes.
This project is licensed under the terms of the
[](https://github.com/mimshins/utilityjs/blob/main/LICENSE).