Wordle in Remix: Part 9 - Validation

Wordle in Remix: Part 9 - Validation

ยท

6 min read

This is the ninth article in a series where we create a Wordle clone in Remix! ๐Ÿ’ฟ We go step by step, from setting up the application to creating pages and implementing game logic. In this article, we will try to prevent the most frequently occurring errors in our game by adding server validation and proper alerts in the browser. Let's go!

Current problems

We've got a bunch of smaller problems which you probably noticed during the development. Here's a short summary of those.

  • Submitting an empty guess. Currently, you can press enter and cause the form to submit even if you haven't input the word.
  • Evaluating a guess shorter than 5 letters. We've already put a constraint on our guess input for the maximum length to be 5 letters. But shorter guesses are still considered valid.
  • Submitting a random word. Currently, you can input a random string of letters that's not a valid word. We should limit this only to words that actually occur in English.

Let's take these issues one by one and implement the necessary validation. โœ…

Server-side validation

We will start by adding the validation on the backend side. When a user submits an empty guess, we want to return earlier with some message for the user. Also, since this is an error, we might want to indicate that by changing the status code to 400.

Up until this point, we were only using redirect in action handlers but we can also use json, the same as in loaders, to return error data with the correct status. Let's give that a go!

// app/routes/play.tsx
...
export const action: ActionFunction = async ({ request }) => {
  ...
  if (_action === "reset") {
    return redirect("/play", {
      headers: { "Set-Cookie": await destroySession(session) },
    });
  }

  if (word === "") {
    return json(
      { error: "Guess cannot be empty" },
      {
        status: 400,
        headers: { "Set-Cookie": await commitSession(session) },
      }
    );
  }
  ...
};

Now, when you submit an empty guess and look into the Network tab in your browser, you should see a rejected request with the error message, just like so.

Network Tab with Error

While it's great that we can see the error in the Network tab, the users would probably appreciate it more if we displayed the message on the interface instead - let's do that. ๐Ÿ‘

Hooking into the error data

Similar to reading the data that's returned from the loader function with useLoaderData, we can read the data returned from the action function with useActionData hook from Remix. Based on that, we can extend the status of our view.

// app/routes/play.tsx
...
export default function Play() {
  ...
  const actionData = useActionData();
  const status = transition.submission
    ? "loading"
    : actionData?.error
    ? "error"
    : "idle";
  ...
}

Contrary to useLoaderData which, if a loader is defined for a route, always returns data, useActionData only gives us something when the user has used that action - that's why we need to check if the return value of that hook is defined.

With that in place, when we're in the error state, let's display a DismissableAlert with the message (like always, here's the implementation of the component).

// app/routes/play.tsx
import { DismissableAlert } from "~/components/DismissableAlert";
...
export default function Play() {
  ...
  return (
    <main className="my-8 mx-4">
      ...
      <div className="flex justify-center">
        <div>
          ...
          {status === "error" && (
            <div className="mt-8">
              <DismissableAlert status="error">
                {actionData.error}
              </DismissableAlert>
            </div>
          )}
        </div>
      </div>
      ...
    </main>
  );
}

Now when you submit an empty guess, there's a nice alert displayed for the user.

Empty Guess Alert

You can dismiss it by clicking the close button. Also, because it's only rendered when we are in the error state, it goes away when you submit an actual word. Awesome! ๐Ÿ™Œ

Early client-side validation

In this case, we can also introduce an early validation in the browser. We might want to skip submitting the form when the input is empty. Let's do that - once again we rely on native HTML Form API and prevent the submission if a user hasn't typed in the word.

// app/components/form/InputForm.tsx
...
export function InputForm({ input, setInput, disabled }: InputFormProps) {
  ...
  return (
    <Form
      method="post"
      autoComplete="off"
      className="h-0 overflow-hidden"
      onSubmit={(e) => {
        if (input.length === 0) {
          e.preventDefault();
        }
      }}
    >
      ...
    </Form>
  );
}

With that, when the input is empty and we hit enter, we no longer get an error. Even though the server validation is no longer used, we might want to leave it in place for completeness.

Let's now handle the remaining two issues, shall we?

Checking for the correct length

First, let's return an error when the submitted word is shorter than 5 letters. For completeness and full type safety, we will also return an error when a submitted word is not a string.

// app/routes/play.tsx
...
export const action: ActionFunction = async ({ request }) => {
  ...
  if (typeof word !== "string") {
    return json(
      { error: "Guess must be of type string" },
      {
        status: 400,
        headers: { "Set-Cookie": await commitSession(session) },
      }
    );
  }
  if (word === "") {
    return json(
      { error: "Guess cannot be empty" },
      {
        status: 400,
        headers: { "Set-Cookie": await commitSession(session) },
      }
    );
  }
  if (word.length !== 5) {
    return json(
      { error: "Guess must be of length 5" },
      {
        status: 400,
        headers: { "Set-Cookie": await commitSession(session), },
      }
    );
  }
  ...
}

We should see the following when we submit a word that is shorter than 5 letters.

Wrong Word Length Alert

Checking if the word exists

One last thing to check is whether the submitted word is valid. The words.ts module exposes a function that verifies if the word exists in English.

Disclaimer: we are using a very naive and simplified implementation including a hardcoded list of words, just for development purposes. You might want to extend this list to contain more valid words or change the implementation to use some open dictionary API to verify the word.

Let's use this function as the last step of our validation.

// app/routes/play.tsx
import { inWordList } from "~/words";
...
export const action: ActionFunction = async ({ request }) => {
  ...
  if (!inWordList(word)) {
    return json(
      { error: "Guess is not in word list" },
      {
        status: 400,
        headers: {
          "Set-Cookie": await commitSession(session),
        },
      }
    );
  }
  ...
};

Let's try submitting a string that's not a valid word in English.

Invalid Word Alert

Just what we'd expect, great! ๐Ÿฅณ

Conclusion

And this is it, we've added validation to our application and surfaced errors that might occur to the user. โœจ Here's a short summary of things we've gone through:

  • outlined the known issues that could happen in the game
  • added server-side validation in the main action handler
  • used useActionData to handle the returned errors on the client-side and error alerts

We are really close to wrapping up the entire project - in the last article, we will handle other, uncaught errors that might happen in our application. See you! ๐Ÿ‘‹

If you liked the article or you have a question, feel free to reach out to me on Twitterโ€š or add a comment below!

Further reading and references

ย