Skip to content

Commit dbd47ba

Browse files
committed
feat: add useTimer
1 parent 987e610 commit dbd47ba

File tree

5 files changed

+164
-0
lines changed

5 files changed

+164
-0
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export * from "./use-throttle-callback";
3535
export * from "./use-throttle-effect";
3636
export * from "./use-throttle-state";
3737
export * from "./use-timeout";
38+
export * from "./use-timer";
3839
export * from "./use-unmount-effect";
3940
export * from "./use-update";
4041
export * from "./use-update-effect";

src/use-timer/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
## 🪝 `useTimer`
2+
3+
```ts
4+
function useTimer(initialValue?: number): Timer;
5+
```
6+
7+
Returns a timer whose `value` field is a binding that will update every frame on Heartbeat. The timer's `value` field will start at `initialValue` if provided, or `0` otherwise.
8+
9+
By default, the timer will start when the component mounts. If you want to start or stop the timer later, you can use the `start` and `stop` methods.
10+
11+
### 📕 Parameters
12+
13+
- `initialValue` - An optional initial value for the timer. Defaults to `0`.
14+
15+
### 📗 Returns
16+
17+
- A `Timer` object:
18+
- `value` - A binding that will update every frame.
19+
- `start()` - Starts the timer if it is not already running.
20+
- `stop()` - Stops the timer if it is running.
21+
- `reset()` - Resets the value to `0`.
22+
- `set(value)` - Sets the value to a new value.
23+
24+
### 📘 Example
25+
26+
```tsx
27+
function Blink() {
28+
const timer = useTimer();
29+
const transparency = timer.value.map((t) => math.sin(t) * 0.5 + 0.5);
30+
31+
return (
32+
<textbutton
33+
BackgroundTransparency={transparency}
34+
Event={{
35+
Activated: () => timer.reset(),
36+
}}
37+
/>
38+
);
39+
}
40+
```

src/use-timer/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./use-timer";

src/use-timer/use-timer.spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/// <reference types="@rbxts/testez/globals" />
2+
3+
import { renderHook } from "../utils/testez";
4+
import { useTimer } from "./use-timer";
5+
6+
export = () => {
7+
it("should start the timer on mount", () => {
8+
const { result, unmount } = renderHook(() => useTimer());
9+
10+
expect(result.current.value.getValue()).to.equal(0);
11+
12+
const timePassed = task.wait(0.2);
13+
expect(result.current.value.getValue()).to.be.near(timePassed, 0.08);
14+
15+
unmount();
16+
});
17+
18+
it("should return functions to start and stop the timer", () => {
19+
const { result, unmount } = renderHook(() => useTimer());
20+
21+
expect(result.current.value.getValue()).to.equal(0);
22+
23+
const timePassed = task.wait(0.2);
24+
const timerValue = result.current.value.getValue();
25+
expect(timerValue).to.be.near(timePassed, 0.08);
26+
27+
result.current.stop();
28+
29+
task.wait(0.2);
30+
expect(result.current.value.getValue()).to.equal(timerValue);
31+
32+
result.current.start();
33+
34+
const timePassedAfterStart = task.wait(0.2);
35+
expect(result.current.value.getValue()).to.be.near(timePassed + timePassedAfterStart, 0.08);
36+
37+
unmount();
38+
});
39+
40+
it("should return a function to set the timer", () => {
41+
const { result, unmount } = renderHook(() => useTimer());
42+
43+
expect(result.current.value.getValue()).to.equal(0);
44+
45+
const timePassed = task.wait(0.2);
46+
expect(result.current.value.getValue()).to.be.near(timePassed, 0.08);
47+
48+
result.current.reset();
49+
expect(result.current.value.getValue()).to.equal(0);
50+
51+
result.current.set(1);
52+
expect(result.current.value.getValue()).to.equal(1);
53+
54+
const timePassedAfterSet = task.wait(0.2);
55+
expect(result.current.value.getValue()).to.be.near(timePassedAfterSet + 1, 0.08);
56+
57+
unmount();
58+
});
59+
};

src/use-timer/use-timer.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import Roact from "@rbxts/roact";
2+
import { useBinding, useCallback, useMutable } from "@rbxts/roact-hooked";
3+
import { RunService } from "@rbxts/services";
4+
import { useEventListener } from "../use-event-listener";
5+
6+
export interface Timer {
7+
/**
8+
* A binding that represents the current value of the timer.
9+
*/
10+
readonly value: Roact.Binding<number>;
11+
/**
12+
* Starts the timer if it is not already running.
13+
*/
14+
readonly start: () => void;
15+
/**
16+
* Pauses the timer if it is running.
17+
*/
18+
readonly stop: () => void;
19+
/**
20+
* Resets the timer to 0.
21+
*/
22+
readonly reset: () => void;
23+
/**
24+
* Sets the timer to a specific value.
25+
* @param value The value to set the timer to.
26+
*/
27+
readonly set: (value: number) => void;
28+
}
29+
30+
/**
31+
* Creates a timer that can be used to track a value over time.
32+
* @param initialValue The initial value of the timer.
33+
* @returns A timer object.
34+
*/
35+
export function useTimer(initialValue = 0): Timer {
36+
const [value, setValue] = useBinding(initialValue);
37+
38+
const started = useMutable(true);
39+
40+
useEventListener(RunService.Heartbeat, (deltaTime) => {
41+
if (started.current) {
42+
setValue(value.getValue() + deltaTime);
43+
}
44+
});
45+
46+
const start = useCallback(() => {
47+
started.current = true;
48+
}, []);
49+
50+
const stop = useCallback(() => {
51+
started.current = false;
52+
}, []);
53+
54+
const reset = useCallback(() => {
55+
setValue(0);
56+
}, []);
57+
58+
const set = useCallback((value: number) => {
59+
setValue(value);
60+
}, []);
61+
62+
return { value, start, stop, reset, set };
63+
}

0 commit comments

Comments
 (0)