useRef
Create a mutable reference that persists across executions
Import
import { useRef } from '@bedrock-core/ui';
Signature
function useRef<T>(initialValue: T): { current: T }
Parameters
initialValue
- Type:
T(generic) - Description: The initial value for the ref's
currentproperty
Returns
A ref object with a single property:
current- The current value (mutable)
Usage
import { system } from '@minecraft/server';
function Timer() {
const intervalRef = useRef<number | null>(null);
const [count, setCount] = useState(0);
const startTimer = () => {
intervalRef.current = system.runInterval(() => {
setCount(c => c + 1);
}, 20); // Runs every 20 ticks (1 second)
};
const stopTimer = () => {
if (intervalRef.current !== null) {
system.clearRun(intervalRef.current);
intervalRef.current = null;
}
};
return (
<>
<Text x={10} y={10} width={200} height={30}>{`Count: ${count}`}</Text>
<Button x={10} y={50} width={120} height={40} onPress={startTimer}>
<Text x={10} y={10} width={100} height={20}>Start</Text>
</Button>
<Button x={140} y={50} width={120} height={40} onPress={stopTimer}>
<Text x={10} y={10} width={100} height={20}>Stop</Text>
</Button>
</>
);
}
Key Characteristics
- Mutable - You can change
.currentdirectly - Persistent - Value persists across executions
- Same reference - Returns the same ref object on every execution
Examples
Store Previous Value
function PreviousValue() {
const [count, setCount] = useState(0);
const prevCountRef = useRef(0);
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<>
<Text x={10} y={10} width={200} height={30}>{`Current: ${count}`}</Text>
<Text x={10} y={40} width={200} height={30}>{`Previous: ${prevCountRef.current}`}</Text>
<Button
x={10} y={80} width={200} height={40}
onPress={() => setCount(count + 1)}
>
<Text x={10} y={10} width={180} height={20}>Increment</Text>
</Button>
</>
);
}
Store Timeout/Interval ID
function DelayedMessage() {
const [message, setMessage] = useState('');
const timeoutRef = useRef<number | null>(null);
const scheduleMessage = () => {
// Clear any existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setMessage('Hello after 2 seconds!');
}, 2000);
};
const cancelMessage = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
setMessage('Cancelled');
}
};
return (
<>
<Text x={10} y={10} width={300} height={30}>{message || 'No message'}</Text>
<Button x={10} y={50} width={180} height={40} onPress={scheduleMessage}>
<Text x={10} y={10} width={160} height={20}>Schedule Message</Text>
</Button>
<Button x={200} y={50} width={180} height={40} onPress={cancelMessage}>
<Text x={10} y={10} width={160} height={20}>Cancel</Text>
</Button>
</>
);
}
Track Execution Count
function ExecutionCounter() {
const executionCount = useRef(0);
const [state, setState] = useState(0);
// Increment on every execution (doesn't cause re-executions)
executionCount.current += 1;
return (
<>
<Text x={10} y={10} width={200} height={30}>{`State: ${state}`}</Text>
<Text x={10} y={40} width={200} height={30}>{`Executions: ${executionCount.current}`}</Text>
<Button
x={10} y={80} width={200} height={40}
onPress={() => setState(state + 1)}
>
<Text x={10} y={10} width={180} height={20}>Update State</Text>
</Button>
</>
);
}
Cache Expensive Computation Result
function ExpensiveComponent({ data }) {
const cacheRef = useRef<Map<string, any>>(new Map());
const getProcessedData = (key: string) => {
if (cacheRef.current.has(key)) {
return cacheRef.current.get(key);
}
// Expensive computation
const result = expensiveOperation(data, key);
cacheRef.current.set(key, result);
return result;
};
return <Text x={10} y={10}>{getProcessedData('key1')}</Text>;
}
Track First Execution
function FirstExecutionDetector() {
const isFirstExecution = useRef(true);
useEffect(() => {
if (isFirstExecution.current) {
console.log('First execution!');
isFirstExecution.current = false;
} else {
console.log('Subsequent execution');
}
});
return <Text x={10} y={10} width={300} height={30}>Check console</Text>;
}
Store Complex Object
interface PlayerData {
id: string;
name: string;
lastSeen: number;
}
function PlayerTracker() {
const playerDataRef = useRef<Map<string, PlayerData>>(new Map());
const addPlayer = (player: Player) => {
playerDataRef.current.set(player.id, {
id: player.id,
name: player.name,
lastSeen: Date.now()
});
};
const getPlayer = (id: string) => {
return playerDataRef.current.get(id);
};
return <Text x={10} y={10}>Tracking {playerDataRef.current.size} players</Text>;
}
Debounce with Ref
function DebouncedInput() {
const [value, setValue] = useState('');
const [debouncedValue, setDebouncedValue] = useState('');
const debounceTimerRef = useRef<number | null>(null);
const handleChange = (newValue: string) => {
setValue(newValue);
// Clear previous timer
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
// Set new timer
debounceTimerRef.current = setTimeout(() => {
setDebouncedValue(newValue);
}, 500);
};
return (
<>
<Text x={10} y={10}>Value: {value}</Text>
<Text x={10} y={40}>Debounced: {debouncedValue}</Text>
</>
);
}
useRef vs useState
| Aspect | useRef | useState |
|---|---|---|
| Re-executions | No | Yes |
| Mutability | Mutable .current | Immutable, use setter |
| Use case | Store mutable data | Manage component state |
| Persistence | Across executions | Across executions |
| Initial value | Directly assigned | Via initializer |
// useState - triggers re-executions
const [count, setCount] = useState(0);
setCount(1); // Re-executes component
// useRef - no re-executions
const countRef = useRef(0);
countRef.current = 1; // No re-executions
Best Practices
When to Use useRef
✅ Use useRef when:
- Storing timer/interval IDs for cleanup
- Tracking previous values
- Caching computed values
- Storing mutable data that doesn't affect execution
- Avoiding stale closures in event handlers
❌ Don't use useRef when:
- The value should trigger a re-execution (use
useState) - You need to trigger effects when value changes (use
useState)
Mutation Guidelines
// ✅ Good - mutate .current directly
const ref = useRef(0);
ref.current = 1;
// ❌ Bad - trying to reassign ref itself
const ref = useRef(0);
ref = { current: 1 }; // Error!
// ✅ Good - use in effect cleanup
import { system } from '@minecraft/server';
useEffect(() => {
const runId = system.runInterval(() => {}, 20);
timerRef.current = runId;
return () => system.clearRun(timerRef.current);
}, []);
Type Safety
// Specify type for better TypeScript support
const timerRef = useRef<number | null>(null);
const dataRef = useRef<PlayerData | undefined>(undefined);
const mapRef = useRef<Map<string, any>>(new Map());
Common Patterns
Cleanup Pattern
const resourceRef = useRef<Resource | null>(null);
useEffect(() => {
resourceRef.current = createResource();
return () => {
resourceRef.current?.cleanup();
resourceRef.current = null;
};
}, []);
Latest Value Pattern
const latestPropsRef = useRef(props);
latestPropsRef.current = props;
// Use latestPropsRef.current in callbacks to avoid stale closures