Skip to main content

πŸš€ Migrating from Loaders/Actions to jods

This guide helps you incrementally migrate your existing Remix application to use jods stores for state management.

πŸ›£οΈ Step-by-Step Migration​

πŸ” 1. Identify Loaders to Convert​

Look for loaders that fetch and return data that would benefit from reactivity:

// Before: Traditional loader
export const loader = async ({ request }) => {
const user = await getUserFromDatabase();
return json({ user });
};

πŸ—οΈ 2. Create a jods Store​

// After: jods store
import { defineStore } from "jods/remix";
import { z } from "zod";

export const user = defineStore({
name: "user",
schema: z.object({
name: z.string(),
email: z.string().email(),
// other fields
}),
loader: async ({ request }) => {
return await getUserFromDatabase();
},
});

// Simplified loader
import { withJods } from "jods/remix";
export const loader = withJods([user]);

πŸ”„ 3. Convert Actions to Handlers​

// Before: Traditional action
export const action = async ({ request }) => {
const form = await request.formData();
const name = form.get("name");
await updateUser({ name });
return redirect("/profile");
};

// After: jods handler
export const user = defineStore({
// ...existing store config
handlers: {
async updateProfile({ current, form }) {
const name = form.get("name");
await updateUser({ name });
return { ...current, name };
},
},
});

🧩 4. Update Components​

// Before: Using useLoaderData
import { useLoaderData } from "@remix-run/react";

export default function Profile() {
const { user } = useLoaderData<typeof loader>();

return (
<div>
<h1>Hello, {user.name}</h1>
<Form method="post">
<input name="name" defaultValue={user.name} />
<button type="submit">Update Profile</button>
</Form>
</div>
);
}

// After: Using jods with the recommended useJods hook
import { useJods } from "jods/remix";
import { user } from "~/jods/user.jods";

export default function Profile() {
const { stores, actions, loaderData } = useJods(user, ["updateProfile"]);

return (
<div>
<h1>Hello, {stores.name}</h1>
<actions.updateProfile.Form>
<input name="name" defaultValue={stores.name} />
<button type="submit">Update Profile</button>
</actions.updateProfile.Form>
</div>
);
}

πŸ”„ Incremental Adoption with connectActionToJods​

Sometimes you can't migrate a whole action at once. Use connectActionToJods to gradually adopt jods:

import { connectActionToJods } from "jods/remix";
import { user } from "~/jods/user.jods";

// Keep your existing action
const existingAction = async ({ request }) => {
const form = await request.formData();
const name = form.get("name");
const updatedUser = await updateUser({ name });
return updatedUser;
};

// Connect it to jods
export const action = connectActionToJods(user, existingAction);

βœ… Migration Checklist​

  • πŸ” Identify state that would benefit from reactivity
  • πŸ“ Define schemas using Zod for type safety
  • πŸ—οΈ Create jods stores for each data domain
  • πŸ”„ Convert loaders to use withJods
  • πŸ› οΈ Migrate actions to store handlers
  • 🧩 Update components to use useJods hook

πŸ”€ Handling Redirects​

When you need to redirect after a form submission:

import { redirect } from "@remix-run/node";

export const user = defineStore({
// ...
handlers: {
async createUser({ current, form }) {
const user = await createUserInDb({
name: form.get("name"),
email: form.get("email"),
});

// Throw a redirect instead of returning state
// This will be handled by Remix
throw redirect(`/users/${user.id}`);
},
},
});

πŸ”’ Handling Authorization​

For protected routes and data:

export const protectedData = defineStore({
name: "protectedData",
schema: z.object({
/* ... */
}),
loader: async ({ request }) => {
// Check authentication
const user = await authenticateUser(request);
if (!user) {
throw redirect("/login");
}

// Get data for authenticated user
return getProtectedData(user.id);
},
});

πŸ”„ Migrating from Individual Hooks to useJods​

If you're already using jods with the individual hooks (useJodsStore and useJodsForm), here's how to migrate to the new unified useJods hook:

Before: Using Individual Hooks​

import { useJodsStore, useJodsForm } from "jods/remix";
import { user } from "~/jods/user.jods";
import { useLoaderData } from "@remix-run/react";

export default function Profile() {
// Access store data
const userData = useJodsStore(user);

// Get loader data separately
const loaderData = useLoaderData();

// Access form handlers separately
const updateProfileForm = useJodsForm(user, "updateProfile");
const changePasswordForm = useJodsForm(user, "changePassword");

return (
<div>
<h1>{loaderData.title}</h1>
<p>Hello, {userData.name}</p>

<updateProfileForm.Form>
<input name="name" defaultValue={userData.name} />
<button type="submit">Update Profile</button>
</updateProfileForm.Form>

<changePasswordForm.Form>
<input type="password" name="password" />
<button type="submit">Change Password</button>
</changePasswordForm.Form>
</div>
);
}

After: Using the Unified useJods Hook​

import { useJods } from "jods/remix";
import { user } from "~/jods/user.jods";

export default function Profile() {
// Get everything in one hook call
const { stores, actions, loaderData } = useJods(user, [
"updateProfile",
"changePassword",
]);

return (
<div>
<h1>{loaderData.title}</h1>
<p>Hello, {stores.name}</p>

<actions.updateProfile.Form>
<input name="name" defaultValue={stores.name} />
<button type="submit">Update Profile</button>
</actions.updateProfile.Form>

<actions.changePassword.Form>
<input type="password" name="password" />
<button type="submit">Change Password</button>
</actions.changePassword.Form>
</div>
);
}

Benefits of Using the Unified Hook​

  • βœ… Less Code: Fewer imports and hook calls
  • βœ… Better Organization: All related functionality grouped together
  • βœ… Direct Mutations: Ability to directly update properties (stores.name = "New Name")
  • βœ… Automatic Access to Loader Data: No need for separate useLoaderData
  • βœ… Improved DX: Actions organized by name for better discoverability