DEV Community

Cover image for Try a React component without creating a project
JSLab
JSLab

Posted on

Try a React component without creating a project

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;
}
Enter fullscreen mode Exit fullscreen mode

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.
  • ms belongs in the dependency array. Leave it out and changing the delay does nothing until value changes 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

A JSLab Browser tab set to TSX and React. The code defines a Counter component with useState and useEffect, ticking every 500 ms, and mounts it with createRoot. The Web View on the right shows Ticks: 24.

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);
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)