This is the sixth 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 implement resetting the game and look into how we can handle multiple actions on a single route. Let's start!
Let me play once more
In the original Wordle game, the user can only play once per 24 hours. Our implementation will be less strict - we allow the user to reset the game at any point and play once again with a new word.
We will start by implementing the reset action on win and loss pop-ups. Resetting the game in our case means doing two things:
- removing the existing session cookie
- redirecting to the
/play
route
Reset action
We already know what's needed for handling actions in Remix - a form to trigger the action and an action handler on the server to process it. Let's go to app/routes/play/win.tsx
and wrap the Play again
button in a form first and change the button type. Contrary to the input form, here we don't need any input, as we won't be passing any data for that action.
// app/routes/play/win.tsx
...
export default function Win() {
...
return (
<Dialog onClose={onClose}>
<div className="text-center">
...
<form method="post">
<Button type="submit">Play again</Button>
</form>
</div>
</Dialog>
);
}
Here we used a regular HTML form
element - as mentioned before, the APIs of the Remix Form
and the native one are very much the same. The latter gives us all that we need for this action.
Now, let's add an action handler. We will read the session and redirect to /play
page, clearing the existing cookie. You should know all that you need if you followed the previous articles.
// app/routes/play/win.tsx
import { ActionFunction, redirect } from "remix";
import { destroySession, getSession } from "~/sessions";
...
export const action: ActionFunction = async ({ request }) => {
const session = await getSession(request.headers.get("Cookie"));
return redirect("/play", {
headers: { "Set-Cookie": await destroySession(session) },
});
};
Now, when you go to /play/win
route and click Play again
, it redirects you back to a clear game board and you can play again! 🥳
It works that way because when we remove the cookie and redirect to the /play
route, the loader function on that route first checks whether a session exists. In our case, it does not, since we just cleared it, so we get a fresh new session cookie with a new random word.
Now, you can apply the same changes to the /play/loss
route - there's no difference in the code needed there.
This one's too hard
We also want to give the user a chance to say "Oh, I can't get this one. I have no idea what it is, so before I lose, I'd like to start again". We've got you covered - we will add a reset button on the game page as well.
Multiple actions on a single route
Although we can render as many forms as we want on a single route, the Route Module API has a limitation that you can only define a single action
function per route. On the /play
route we already have an action defined, allowing the user to verify the user's guess.
We can make the Remix form submit to a different route with <Form action="/play/reset" />
, but for that, we would have to implement this route just to be able to handle an action there - it wouldn't do anything else. However, there's another way we can approach it, making the existing action
function handle two different actions.
Adding a new form component
Let's put in a new form component on the page, with some additional CSS classes in the wrapping elements. Note that we want to disable this form when the user hasn't made any guess yet, so there's nothing to reset.
// app/routes/play.tsx
import { ResetForm } from "~/components/form/ResetForm";
...
export default function Play() {
...
return (
<main className="my-8 mx-4">
...
<div className="flex justify-center">
<div>
<div className="mb-8 flex items-center gap-4 justify-between">
<span>
Press <Mark>Enter</Mark> to submit...
</span>
<ResetForm disabled={!resolvedGuesses.length} />
</div>
...
</div>
</div>
<Outlet />
</main>
);
}
We will add the implementation - let's create a new file with the ResetForm
, which accepts a disabled
prop. It will render a form with a single Reset
button.
// app/components/form/ResetForm.tsx
import { Button } from "~components/Button";
interface ResetFormProps {
disabled: boolean;
}
export function ResetForm({ disabled }: ResetFormProps) {
return (
<form method="post">
<Button
type="submit"
variant="secondary"
disabled={disabled}
>
Reset
</Button>
</Form>
);
}
Once that's done, we should see the following in the browser.
If you now click on the reset button, you get an error. We just triggered the action that exists on this route. It tries to validate the guess, while the form we just submitted doesn't send any data. Ouch. 🙊
Button attributes
We need our action handler to know what the user's intention is - whether that's a new guess submission or a reset request. HTML to the rescue (once again). 🚀
We can send additional data with the form inputs by adding attributes to the button
element. By adding name
and value
we can submit any arbitrary data when that button triggers the submission (name
will be the property key and value
will be the value). Let's add these two to the button.
export function ResetForm({ disabled }: ResetFormProps) {
return (
<Form method="post">
<Button
...
name="_action"
value="reset"
>
Reset
</Button>
</Form>
);
}
Now let's add the appropriate logic to the action function. When the action is reset
, we want to do the same as what we did for win and loss routes - redirect to /play
, clearing the session cookie. Otherwise, we continue with the guess-checking logic.
import { destroySession } from "~/sessions";
...
export const action: ActionFunction = async ({ request }) => {
const session = await getSession(request.headers.get("Cookie"));
const formData = await request.formData();
const { word, _action } = Object.fromEntries(formData);
if (_action === "reset") {
return redirect("/play", {
headers: { "Set-Cookie": await destroySession(session) },
});
}
...
};
Now, when the user clicks the button, they got another word to guess, which is what we wanted. 🙌
Conclusion
And that's it, the user can play our game as long as they want! ✨ Here's a short summary of things we've gone through:
- implementing game reset action from win and loss pop-ups
- discussing how we can handle multiple actions on one route in Remix
- implementing game reset action on the main game page using additional button attributes
Next up, we will be adding protections to selected routes in our application to make sure the users can't access some pages too early. 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
- Full code available in this repository
- Photo by Mathyas Kurmann on Unsplash
- Remix documentation