← All blog posts

Adding client-side interaction to Astro

2022-09-20

So, after getting this site up and running on Astro I wanted to explore a bit more and see where the limits of the framework are. Obviously Astro performs very well as a static site generator and produces minimal or zero JavaScript bundle sizes even though much of the site is built with React components. But what if we wanted to force in a little bit of client-side interaction?

My idea to introduce this was with a form that filters projects I’ve worked on based on a search phrase. I wanted to this as simple as possible, with no external libs if possible, once again to limit the payload. There’s no validation needed, no complicated form state to handle - just an input field and some buttons. The first version looked something like this:

export const Search = () => {
  return (
    <form>
      <input id="search" type="search" name="search"></input>
      <Button type="submit">Filter</Button>
    </form>
  );
};

The funny this about this is that when submitted it will GET itself to the same URL as it’s on, but with

?search=<your query>

which works very well for server-rendering the same page but with filtering on projects. Oh, how we’re back at the PHP pages of old :heart-eyes: (was that good, though :thinking:?).

So, here I opted to add an external lib to get the highlighting working: react-highlighter.

So, all of the above works without any client-side code really, it works with JavaScript disabled in Chrome.

It works by on the index.astro page reading the query parameters:

const search = Astro.url.searchParams.get("search") || "";

Then passing that to the Resume component:

<Resume search={search} />
export const Resume = ({ search = "" }: Props) => {
  /* ... */

  return (
    <>
      // ...
      <Search initialValue={search} />
      // ...
    </>
  );
};

and finally back to a modified Search component that sets this as defaultValue:

export const Search = ({ initialValue = "" }: SearchProps) => {
  return (
    <form>
      <input
        id="search"
        type="search"
        name="search"
        defaultValue={initialValue || ""}
      ></input>
      <Button type="submit">Filter</Button>
    </form>
  );
};

However, the defaultValue introduced a problem. I wanted to have a Clear button as well, and an

<input type="reset" value="Clear" />

element would simply reset the value to whatever was previously filtered on, since I set that value to defaultValue. Perhaps there are other solutions to this, but what I ended up doing was creating a custom button click handler that empties the form value before submitting it. Obviously this doesn’t work without JavaScript, so here we are already moving away from a server-side-only working web page…

export const Search = ({ initialValue = "" }: SearchProps) => {
  c;
  const formRef = useRef<HTMLFormElement>(null);
  const searchRef = useRef<HTMLInputElement>(null);

  const onHandleReset = () => {
    if (!searchRef || !searchRef.current) return;
    if (!formRef || !formRef.current) return;
    searchRef.current.setAttribute("value", "");
    formRef.current.submit();
  };

  return (
    <form ref={formRef}>
      <input
        type="search"
        name="search"
        defaultValue={initialValue || ""}
        ref={searchRef}
      ></input>
      <Button type="submit">Filter</Button>
      <Button onClick={onHandleReset}>Clear</Button>
    </form>
  );
};

So, now we have a way of filtering content on the page. To get the interaction working client-side you need to tell Astro to load JS code:

<Resume search={search} client:visible />

What still doesn’t work is that since the page is reloaded every time the form is submitted, the user is scrolled to the top of the page which is a bit annoying… Ah, the UX we have adjusted ourselves to with JS is seldom thought of, but becomes quite obvious once you remove it.

To be continued!

← All blog posts