Search code examples
javascriptreactjstypescriptreact-routerreact-router-dom

How to use react-router useSubmit hook with input onChange and button onClick events?


I am trying to make a search bar with a clear button using the useSubmit hook from react-router. The search query should be submitted both on input change and button click.

SearchBar.tsx

import { Form, useSubmit } from 'react-router-dom';
import { ChangeEvent, useState } from 'react';

export default function SearchBar({ query }: { query?: string }) {
  const submit = useSubmit();
  const [q, setQ] = useState(query ?? '');

  function handleChange(event: ChangeEvent<HTMLInputElement>) {
    setQ(event.target.value);
    submitForm(event.currentTarget.form);
  }

  function handleClear(event: any) {
    setQ('');
    submitForm(event.currentTarget.form);
  }

  function submitForm(submitTarget: any) {
    const isFirstSearch = q == null;
    submit(submitTarget, {
      replace: !isFirstSearch,
    });
  }

  return (
    <Form role="search">
      <input
        type="search"
        name="q"
        value={q}
        onChange={handleChange}
        autoComplete="off"
      />
      {q && q.length > 0 && (
        <button type="button" onClick={handleClear}>
          Clear
        </button>
      )}
    </Form>
  );
}

todos.tsx

import { getTodos } from '../todos';
import { useLoaderData } from 'react-router-dom';
import SearchBar from '../SearchBar';

export async function loader({ request }: { request: Request }) {
  const url = new URL(request.url);
  const q = url.searchParams.get('q');
  const todos = await getTodos(q);
  return { todos, q };
}

export default function Todos() {
  const { todos, q }: any = useLoaderData();

  return (
    <div>
      <SearchBar query={q} />
      <div>
        {todos.length > 0 ? (
          todos.map((todo: any) => <div key={todo.id}>{todo.text}</div>)
        ) : (
          <p>No todos</p>
        )}
      </div>
    </div>
  );
}

When I click the clear button, the input gets cleared but the submitted query has still the value from before.

StackBlitz


Solution

  • Here's what I was able to come up with that worked for the two use cases you describe.

    1. Move the calling of submitForm to the Form component's onChange handler to submit the form data when it changes.
    2. The input element's onChange handler only updates the local q state and the value prop is removed to now be an uncontrolled input. We only need the q state to conditionally render the "clear" button.
    3. The "clear" button should be converted to a type="submit" to then also submit the form when clicked.
    4. The handleClear handler should reset the form field data.
    export default function SearchBar({ query }: Props) {
      const submit = useSubmit();
      const [q, setQ] = useState(query ?? '');
    
      function handleChange(event: ChangeEvent<HTMLInputElement>) {
        setQ(event.target.value);
      }
    
      function handleClear(event: any) {
        event.currentTarget.form.reset();
      }
    
      function submitForm(event: any) {
        const isFirstSearch = q == null;
        submit(event.currentTarget, {
          replace: !isFirstSearch,
        });
      }
    
      return (
        <Form role="search" onChange={submitForm}>
          <input
            type="search"
            name="q"
            onChange={handleChange}
            autoComplete="off"
          />
          {!!q.length && (
            <button type="submit" onClick={handleClear}>
              Clear
            </button>
          )}
        </Form>
      );
    }