# react-timer-hook: countdown, stopwatch and clock hooks anchored to timestamps

> react-timer-hook is a zero-dependency React and React Native hook library offering useTimer, useStopwatch and useTime. The interesting design decision is that the countdown is computed from an expiry timestamp, so the interval setting only controls how often the display is refreshed.

**amrlabib/react-timer-hook** — Lightweight, zero-dependency, drift-free, millisecond-precision custom React Hook for timer (countdown), stopwatch, and time logic/state in your React and React Native components.

- Repository: https://github.com/amrlabib/react-timer-hook
- Website: https://amrlabib.github.io/react-timer-hook/
- Stars: 624 · Forks: 116
- Language: TypeScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/amrlabib-react-timer-hook

## Three hooks for three different kinds of time

Counting time in a React component is one of those chores that looks small until a component unmounts, a tab is backgrounded, or the user pauses halfway through a checkout step. react-timer-hook collects that work into three hooks: `useTimer` for a countdown, `useStopwatch` for a count-up timer, and `useTime` for the current time. The project describes the package as lightweight, zero-dependency and usable from both React and React Native components, and the topic tags on the project name React and React Native explicitly.

The audience is therefore front-end developers who need a timer in the UI rather than a timing utility in Node. There is no server component, no scheduler and no runtime beyond React itself. The published bundle is advertised at 1.5kB gzipped, and the package ships type declarations alongside the JavaScript entry point, so a TypeScript project gets the hook signatures without an extra types package.

## The expiry timestamp is the anchor, not a running count

The mechanism behind the drift-free claim is visible in the API. A countdown is started with an `expiryTimestamp`, a `Date` object that marks when the timer should end, and every value the hook exposes is derived from the distance between that date and the current time. Nothing in the settings table counts ticks and decrements a counter.

That design has a practical consequence. If the browser throttles a timer in a background tab, or the main thread is busy, the displayed value can skip, but the number of milliseconds remaining is still computed against a fixed wall clock deadline. The alternative, a counter that decrements once per interval, drifts by whatever the scheduling delay was.

The countdown values are split into `totalSeconds` and `totalMilliseconds`, both described as the total remaining time and not converted into minutes, hours or days, alongside the presentation fields `seconds`, `minutes`, `hours`, `days` and `milliseconds`. The `isRunning` flag tells the component whether to show a running state, and the hook returns `start`, `pause`, `resume` and `restart` as functions the component wires straight into button handlers.

## The interval setting samples the clock, it does not define it

This is the detail most likely to surprise you, and it is stated directly in the settings table for both `useTimer` and `useStopwatch`. The `interval` value does not affect the timer itself. It only defines the frequency used to calculate the current timer values. The default is 1000ms, so the component re-renders once per second, and the `milliseconds` field is documented as needing a smaller interval such as 20ms or 100ms to be accurate.

So a countdown that displays `milliseconds` on a 1000ms interval will show a value that only changes every second, and the milliseconds digits will read as a stale sample rather than a live reading. The demo in the project documentation asks for `interval: 20`, which is a 50 times higher render frequency than the default for the sake of one visible column.

There is a cost to that setting that the documentation does not spell out: every sample produces a state update and a re-render of the consuming component. At 20ms the hook asks React to re-render fifty times per second, which is a real load on a component tree that does nothing else. The safer arrangement, if you need smooth milliseconds, is to keep the countdown interval at its default and render the millisecond digits from a separate, narrowly scoped component so only that subtree re-renders. The hook itself gives you no way to isolate the render cost, because the values and the state live in the same hook call.

## Installing the package and rendering a first countdown

The package is published on npm, and the setup instructions give two equivalent commands. Use yarn:

```bash
yarn add react-timer-hook OR npm install --save react-timer-hook
```

The hook is then imported into a component. This is the shape of the documented example, with the returned values pulled out of `useTimer`:

```javascript
import React from 'react';
import { useTimer } from 'react-timer-hook';

function MyTimer({ expiryTimestamp }) {
  const {
    totalSeconds,
    milliseconds,
    seconds,
    minutes,
    hours,
    days,
    isRunning,
    start,
  } = useTimer({ expiryTimestamp, onExpire: () => console.warn('onExpire called'), interval: 20 });
```

The returned fields map straight onto a display and onto buttons, which is the other half of the example:

```javascript
  return (
    <div style={{textAlign: 'center'}}>
      <h1>react-timer-hook </h1>
      <p>Timer Demo</p>
      <div style={{fontSize: '100px'}}>
        <span>{days}</span>:<span>{hours}</span>:<span>{minutes}</span>:<span>{seconds}</span>:<span>{milliseconds}</span>
      </div>
      <p>{isRunning ? 'Running' : 'Not running'}</p>
      <button onClick={start}>Start</button>
      <button onClick={pause}>Pause</button>
      <button onClick={resume}>Resume</button>
```

`expiryTimestamp` is the one required setting, `autoStart` decides whether the countdown begins on mount and defaults to `true`, and `onExpire` fires once when the countdown reaches zero. Restarting means handing the hook a new expiry date, as in the documented handler:

```javascript
      <button onClick={() => {
        // Restarts to 5 minutes timer
        const time = new Date();
        time.setSeconds(time.getSeconds() + 300);
        restart(time)
      }}>Restart</button>
```

The parent component decides the deadline itself, and the documented example adds ten minutes to the current time before passing the date down:

```javascript
export default function App() {
  const time = new Date();
  time.setSeconds(time.getSeconds() + 600); // 10 minutes timer
  return (
    <div>
      <MyTimer expiryTimestamp={time} />
    </div>
  );
}
```

Because the deadline lives outside the hook, an expiry time that survives a reload or arrives from a server fits the same shape as a countdown that starts at mount.

## start, resume and restart are three different operations

The stopwatch hook returns a similar set of values but a different control set: `start`, `pause` and `reset` rather than `resume` and `restart`. The stopwatch also accepts an `offsetTimestamp`, a date used as the initial offset, and the documentation gives a concrete case where adding 300 seconds to the current time makes the stopwatch start at 0:0:5:0 instead of 0:0:0:0.

For the countdown, the distinction between `start` and `resume` is worth reading twice. `start` continues the timer based on the original `expiryTimestamp`, while `resume` continues the countdown from the last paused state. They are not interchangeable once a pause has happened, and the difference is what makes a resumed timer reflect the pause rather than silently extending the deadline.

`restart` takes two arguments, a new `expiryTimestamp` of type `Date` and an `autoStart` boolean that decides whether the timer starts immediately after the restart, defaulting to `true`. That second argument matters in practice: restarting a countdown that should wait for the user to press start requires passing `false`, otherwise the fresh deadline starts counting down before anyone sees it.

## What the same timer costs when you write it yourself

The realistic alternative is a `useEffect` with a `setInterval` that compares `Date.now()` against an expiry date on each tick, plus a cleanup call in the effect's return. That approach reaches the same result for the simplest countdown and needs no dependency at all, which is a fair point in its favour.

The difference shows up in the details the hook already handles. The hook derives every displayed field, including `totalMilliseconds` and the split `days`, `hours`, `minutes`, `seconds` and `milliseconds` values, from a single anchor rather than from a decrementing counter. It exposes `onExpire` for the moment the deadline passes, `autoStart` so the timer can start on mount without a button press, and `isRunning` so the UI can render a state without keeping its own flag. It also handles the pause and resume distinction, where a hand-rolled version has to decide for itself whether a resumed timer keeps the original deadline or extends it.

The cost of the hook is that its granularity is fixed by a polling interval and it drives re-renders at that rate. The cost of the manual version is that every one of those behaviours is code you write and then maintain. For a plain seconds-only countdown in one component, the manual version is a reasonable choice. For a stopwatch with pause, resume, restart and a millisecond readout, the hook removes a set of decisions that are easy to get subtly wrong.

## Licensing and the version you actually install

The project is listed under the MIT licence, but the published manifest declares a different one. In `package.json` the licence field reads `ISC`, and the README badge points at the repository's `LICENSE` file. Whichever applies, both are permissive licences, but a mismatch like this is the kind of detail worth resolving in writing before a package enters a company codebase, and it is a one-line check against the file you are about to depend on.

The manifest also shows the shape of the package. The JavaScript entry point is `dist/index.js`, the type declarations live at `dist/types/src/index.d.ts`, and the publish step is driven by the `prepare` script that runs a production webpack build. The `build:prod` script removes the old `dist` directory and strips the demo types from the output, so the demo application in `demo/` never reaches consumers.

Development is guarded by the same set of tools the manifest declares. Linting runs through eslint over the `src` directory, tests run under jest with the `@testing-library` packages for React and DOM, and a husky pre-commit hook runs lint, a TypeScript check with `tsc --noEmit`, and the test suite:

```json
    "lint": "node_modules/.bin/eslint src",
    "test": "jest"
```

On maintenance, the repository is not archived and the last push was on 2026-10-20. The release history shows three versions in two days, v4.0.9 on 2026-10-05, then v4.0.8 and v4.0.7 both on 2026-10-04, and the published version is 4.0.9. A project shipping that often will keep changing behaviour, so read the changelog between versions rather than assuming a minor bump is inert.

## Conclusion

Adopt react-timer-hook when you need a countdown, stopwatch or clock display in React or React Native and would rather not manage intervals and expiry dates yourself. Do not adopt it for millisecond displays on the default 1000ms interval, since the milliseconds value is only as fresh as the polling rate, and skip it if you need a timer that keeps running while the component is unmounted, because the documented behaviour does not cover that case. Before shipping, check the licence field in package.json, which declares ISC while the project is listed under MIT, and pin the version you want, since three releases landed on 2026-10-04 and 2026-10-05.

## FAQ

### How do I show milliseconds in react-timer-hook?

Pass a small `interval`, for example 20 or 100, since the default is 1000ms and the `milliseconds` value is only as fresh as the polling rate. The interval controls how often the values are recalculated, not the timer's own deadline.

### What is the difference between start and resume in useTimer?

`start` continues the timer based on the original `expiryTimestamp`, while `resume` continues the countdown from the last paused state. Use `resume` after a pause and `start` when the timer should follow the original deadline.

### Does react-timer-hook work with React Native?

Yes, the project describes the hooks as intended for React and React Native components, and the package topics list react-native among them. The package itself has no dependencies beyond React being present in your app.

### Can I restart a react-timer-hook countdown without starting it immediately?

Yes. `restart` accepts a new `expiryTimestamp` and a second `autoStart` argument, so passing `false` there keeps the timer stopped until you call `start` yourself.

### What licence does react-timer-hook use?

The project is listed under the MIT licence, while the `package.json` manifest declares `ISC`. Both are permissive, but the two files disagree, so check the manifest in the version you install before relying on either.

## Sources

- [amrlabib/react-timer-hook on GitHub](https://github.com/amrlabib/react-timer-hook)
- [License: MIT](https://github.com/amrlabib/react-timer-hook/blob/master/LICENSE)
- [Project website](https://amrlabib.github.io/react-timer-hook/)
- [README](https://github.com/amrlabib/react-timer-hook/blob/master/README.md)
- [Releases](https://github.com/amrlabib/react-timer-hook/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/amrlabib-react-timer-hook
