You want to check how a hook behaves. The usual route is a new project: scaffold, install, start a dev server, delete the boilerplate. That's a lot of ceremony for twenty lines.
Today's twenty lines: a useDebouncedValue hook, the kind you write for a search box so it doesn't filter on every keystroke.
The hook
import { useEffect, useState } from "react"; function useDebouncedValue<T>(value: T, ms = 250): T { const [debounced, setDebounced] = useState(value); useEffect(() => { const id = setTimeout(() => setDebounced(value), ms); return () => clearTimeout(id); }, [value, ms]); return debounced; } How it works: every time value changes, the effect starts a timer. If value changes again before the timer fires, React runs the cleanup first, which cancels the old timer. So debounced only catches up once the input has been still for ms milliseconds.
Two details that bite people:
- The cleanup is the debounce. Without
return () => clearTimeout(id), every keystroke schedules an update and you've built a delay, not a debounce. -
msbelongs in the dependency array. Leave it out and changing the delay does nothing untilvaluechanges too.
A component to try it in
import { useEffect, useMemo, useState } from "react"; // useDebouncedValue from above goes here const fruits = ["apple", "apricot", "banana", "blueberry", "cherry", "grape", "mango"]; export default function App() { const [query, setQuery] = useState(""); const debounced = useDebouncedValue(query, 300); const matches = useMemo( () => fruits.filter((f) => f.includes(debounced.toLowerCase())), [debounced], ); return ( <div style={{ fontFamily: "system-ui", padding: 24 }}> <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Filter fruit" /> <p> {matches.length} of {fruits.length} (typed: "{query}", filtering: "{debounced}") </p> <ul> {matches.map((f) => ( <li key={f}>{f}</li> ))} </ul> </div> ); } Showing both query and debounced on screen is the trick: type fast and you can watch the second one lag behind, then snap into place.
Running it without a project
I ran this in JSLab, a JavaScript & TypeScript scratchpad for macOS. Click + in the tab bar and pick React: JSLab sets the tab's language, runtime and framework, installs the packages it needs, and opens starter code. Paste the file over it.
JSLab renders it into a Web View tile beside the console. Edit the JSX and it rerenders as you type. Change 300 to 1000 and you feel the difference on the next keystroke.
Here's a smaller component running the same way in a real JSLab Browser tab: a Counter mounted with createRoot, ticking in the Web View beside the code.
The console still works alongside it. Add a line like this inside the effect and the logs land in the output panel, tagged with their line:
console.log("debounced →", value); Want styling? Turn on Tailwind in the status bar and the utility classes your code uses get compiled into the page, with nothing to install.
When to make it a real project
A scratchpad is for the question "does this hook behave the way I think?" Once the answer is yes, move it into your app with its tests. The point is not having to pay for a project before you know you need one.
JSLab launches Nov 15. The Web View and framework docs are here: https://js-lab.app/docs/web-view

