Skip to main content

Countdown Timer

This feature will remind you do to something - clean the oven, wash the car, you name it - at a certain time interval. You'll get a notification when the task is due and we'll store the timestamps of past completions in the history page.

Let's start by add a little countdown timer. Given a timestamp, it will print out: how many days, hours, minutes, seconds until the time specified or how long we're overdue.

First, let's see how you might have the UI update every second with the current timestamp:

New file: app/counter/index.tsx
@@ -2,9 +2,21 @@ import { Text, View, StyleSheet, TouchableOpacity, Alert } from "react-native";
import { theme } from "../../theme";
import { registerForPushNotificationsAsync } from "../../utils/registerForPushNotificationsAsync";
import * as Notifications from "expo-notifications";
-import { Countdown } from "../../components/Countdown";
+import { useEffect, useState } from "react";

export default function CounterScreen() {
+ const [secondsElapsed, setSecondsElapsed] = useState(0);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setSecondsElapsed((val) => val + 1);
+ }, 1000);
+
+ return () => {
+ clearInterval(intervalId);
+ };
+ }, []);
+
const scheduleNotification = async () => {
const result = await registerForPushNotificationsAsync();
if (result === "granted") {
@@ -26,7 +38,7 @@ export default function CounterScreen() {

return (
<View style={styles.container}>
- <Countdown />
+ <Text>{secondsElapsed}</Text>
<TouchableOpacity
onPress={scheduleNotification}
style={styles.button}

Now let's install date-fns for making working with dates a bit easier.

npx expo install date-fns

Create this TimerSegment file:

Create: components/TimeSegment.tsx
import { Text, View, StyleSheet, TextStyle } from "react-native";

type Props = {
number: number;
unit: string;
textStyle?: TextStyle;
};

export function TimeSegment({ number, unit, textStyle }: Props) {
return (
<View style={styles.segmentContainer}>
<Text style={[styles.number, textStyle]}>{number}</Text>
<Text style={textStyle}>{unit}</Text>
</View>
);
}

const styles = StyleSheet.create({
segmentContainer: {
padding: 12,
margin: 4,
borderRadius: 6,
justifyContent: "center",
alignItems: "center",
},
number: {
fontSize: 24,
fontWeight: "bold",
fontVariant: ["tabular-nums"],
},
});

And use it in the countdown timer UI:

Update: app/counter/index.tsx
+++ b/app/counter/index.tsx
@@ -3,13 +3,34 @@ import { theme } from "../../theme";
import { registerForPushNotificationsAsync } from "../../utils/registerForPushNotificationsAsync";
import * as Notifications from "expo-notifications";
import { useEffect, useState } from "react";
+import { intervalToDuration, isBefore } from "date-fns";
+import { TimeSegment } from "../../components/TimeSegment";
+
+// 10 seconds from now
+const timestamp = Date.now() + 10 * 1000;
+
+type CountdownStatus = {
+ isOverdue: boolean;
+ distance: ReturnType<typeof intervalToDuration>;
+};

export default function CounterScreen() {
- const [secondsElapsed, setSecondsElapsed] = useState(0);
+ const [status, setStatus] = useState<CountdownStatus>({
+ isOverdue: false,
+ distance: {},
+ });

useEffect(() => {
const intervalId = setInterval(() => {
- setSecondsElapsed((val) => val + 1);
+ const isOverdue = isBefore(timestamp, Date.now());
+
+ const distance = intervalToDuration(
+ isOverdue
+ ? { end: Date.now(), start: timestamp }
+ : { start: Date.now(), end: timestamp },
+ );
+
+ setStatus({ isOverdue, distance });
}, 1000);

return () => {
@@ -37,14 +58,45 @@ export default function CounterScreen() {
};

return (
- <View style={styles.container}>
- <Text>{secondsElapsed}</Text>
+ <View
+ style={[
+ styles.container,
+ status.isOverdue ? styles.containerLate : undefined,
+ ]}
+ >
+ {!status.isOverdue ? (
+ <Text style={[styles.heading]}>Thing due in</Text>
+ ) : (
+ <Text style={[styles.heading, styles.whiteText]}>Thing overdue by</Text>
+ )}
+ <View style={styles.row}>
+ <TimeSegment
+ unit="Days"
+ number={status.distance?.days ?? 0}
+ textStyle={status.isOverdue ? styles.whiteText : undefined}
+ />
+ <TimeSegment
+ unit="Hours"
+ number={status.distance?.hours ?? 0}
+ textStyle={status.isOverdue ? styles.whiteText : undefined}
+ />
+ <TimeSegment
+ unit="Minutes"
+ number={status.distance?.minutes ?? 0}
+ textStyle={status.isOverdue ? styles.whiteText : undefined}
+ />
+ <TimeSegment
+ unit="Seconds"
+ number={status.distance?.seconds ?? 0}
+ textStyle={status.isOverdue ? styles.whiteText : undefined}
+ />
+ </View>
<TouchableOpacity
onPress={scheduleNotification}
style={styles.button}
activeOpacity={0.8}
>
- <Text style={styles.buttonText}>Schedule notification</Text>
+ <Text style={styles.buttonText}>I've done the thing!</Text>
</TouchableOpacity>
</View>
);
@@ -55,6 +107,7 @@ const styles = StyleSheet.create({
flex: 1,
justifyContent: "center",
alignItems: "center",
+ backgroundColor: theme.colorWhite,
},
button: {
backgroundColor: theme.colorBlack,
@@ -67,4 +120,20 @@ const styles = StyleSheet.create({
textTransform: "uppercase",
letterSpacing: 1,
},
+ row: {
+ flexDirection: "row",
+ marginBottom: 24,
+ },
+ heading: {
+ fontSize: 24,
+ fontWeight: "bold",
+ marginBottom: 24,
+ color: theme.colorBlack,
+ },
+ containerLate: {
+ backgroundColor: theme.colorRed,
+ },
+ whiteText: {
+ color: theme.colorWhite,
+ },
});

Now we have a countdown timer that resets every time you open the app and the page changes color when the time is reached.

Checkpoint

AndroidiOS
Checkpoint