Skip to content

Instantly share code, notes, and snippets.

@bjfresh
Created February 28, 2023 21:36
Show Gist options
  • Save bjfresh/0c2d1b8b9c38aef115de70d58de64c48 to your computer and use it in GitHub Desktop.
Save bjfresh/0c2d1b8b9c38aef115de70d58de64c48 to your computer and use it in GitHub Desktop.
Timer App
import React, { useCallback, FormEvent, useState } from "react";
import { useEffect } from "react";
/* helper functions */
const getUUID = () => window.crypto.randomUUID();
const formatTime = (rawSeconds: number) => {
const minutes = Math.floor(rawSeconds / 60);
const seconds = rawSeconds % 60;
return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
};
type Timer = {
name: string;
startTime: number;
};
interface TimerComponentProps {
timer: Timer;
}
const ONE_SECOND = 1000;
// This is pretty scrappy, ideally we'd use something robustly tested and off the shelf
function useInterval(func: () => void, interval: number | null) {
useEffect(() => {
if (!interval || interval === 0) {
return;
}
const activeInterval = setInterval(() => func(), interval);
return () => clearInterval(activeInterval);
}, [func, interval]);
}
// We output a TimerComponent for each row in the list of Timers
function TimerComponent({ timer }: TimerComponentProps) {
const [currentTime, setCurrentTime] = useState<number>(timer.startTime);
const [isRunning, setIsRunning] = useState<boolean>(true);
const handleStart = useCallback(() => {
setIsRunning(true);
}, []);
const handleStop = useCallback(() => {
setIsRunning(false);
}, []);
const handleReset = useCallback(() => {
setCurrentTime(0);
}, []);
useInterval(
() => setCurrentTime(currentTime + 1),
isRunning ? ONE_SECOND : null
);
return (
<div
style={{
display: "flex",
gap: "8px",
alignItems: "center",
padding: "4px",
width: "100%"
}}
>
<div style={{ display: "flex", gap: "8px" }}>
{timer.name} {formatTime(currentTime)}
</div>
<button onClick={handleStart}>Start</button>
<button onClick={handleStop}>Stop</button>
<button onClick={handleReset}>Reset</button>
</div>
);
}
export const App = () => {
const [timers, setTimers] = useState<Timer[]>([]);
const [currentString, setCurrentString] = useState<string>("");
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setCurrentString(event.target.value);
},
[]
);
const handleSubmit = useCallback(
(event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const newTimer = {
name: currentString,
startTime: 0
};
setTimers((prev) => {
return [...prev, newTimer];
});
},
[currentString]
);
return (
<div>
<form name="time" onSubmit={handleSubmit}>
<label htmlFor="timer" style={{ display: "flex", gap: "8px" }}>
<input type="text" name="timer" onChange={handleChange} />
<button type="submit">Add Timer</button>
</label>
</form>
<div>
{timers.map((timer: Timer) => {
return <TimerComponent key={getUUID()} timer={timer} />;
})}
</div>
</div>
);
};
@bjfresh
Copy link
Author

bjfresh commented Feb 28, 2023

For context: the code here is basically functional. One caveat that we’d want to work through is that the useInterval hook doesn’t currently handle state changes + would probably need to be built out much more robustly to maintain a reference so that the timers aren’t lost when the parents re-render. I've tried to keep to a minimal implementation here due to time considerations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment