πΏ πΏοΈ π¦ Remix Integration Guide
π Overviewβ
The jods Remix πΏ integration provides a seamless way to manage your application state across server and client, replacing traditional loaders and actions with reactive stores.
π Getting Startedβ
π¦ Installationβ
pnpm install jods zod
π Basic Setupβ
- Create a jods directory in your app root
- Define your stores using
defineStore - Use the
useJodshook in your components
π Core Conceptsβ
π¦ Defining Storesβ
// app/jods/user.jods.ts
import { defineStore } from "jods/remix";
import { j } from "jods/zod";
export const user = defineStore({
name: "user",
schema: j.object({
name: j.string(),
email: j.string().email(),
preferences: j.object({
theme: j.enum(["light", "dark", "system"]).default("system"),
}),
}),
defaults: {
name: "",
email: "",
preferences: { theme: "system" },
},
handlers: {
async updateProfile({ current, form }) {
return {
...current,
name: form.get("name"),
email: form.get("email"),
};
},
},
loader: async ({ request }) => {
// Load user data from database
return {
name: "Burt Macklin",
email: "burt.macklin@fbi.pawnee.city",
preferences: { theme: "light" },
};
},
});
π£οΈ Using in Routesβ
// app/routes/profile.tsx
import { useJods } from "jods/remix";
import { user } from "~/jods/user.jods";
export default function Profile() {
const { stores, actions } = useJods(user, ["updateProfile"]);
return (
<div>
<h1>Profile</h1>
<actions.updateProfile.Form>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" defaultValue={stores.name} />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" defaultValue={stores.email} />
</div>
<button type="submit">Update Profile</button>
</actions.updateProfile.Form>
</div>
);
}
π Direct Mutations - The jods Wayβ
One of the most powerful features of jods is the ability to directly mutate your store properties:
import { useJods } from "jods/remix";
import { user } from "~/jods/user.jods";
function ThemeToggle() {
const { stores } = useJods(user);
return (
<button
onClick={() => {
// Direct property mutation! πͺ
stores.preferences.theme =
stores.preferences.theme === "dark" ? "light" : "dark";
}}
>
Switch to {stores.preferences.theme === "dark" ? "Light" : "Dark"} mode
</button>
);
}
π§ Advanced Usageβ
β‘ Optimistic UIβ
import { useOptimisticUpdate, useJods } from "jods/remix";
import { cart } from "~/jods/cart.jods";
export function AddToCartButton({ productId, productName }) {
const optimisticCart = useOptimisticUpdate(
cart,
"addItem",
(currentCart) => ({
items: [
...currentCart.items,
{ id: productId, name: productName, quantity: 1 },
],
})
);
const { actions } = useJods(cart, ["addItem"]);
return (
<>
<div className="cart-preview">
{optimisticCart.items.length} items in cart
</div>
<actions.addItem.Form>
<input type="hidden" name="productId" value={productId} />
<button type="submit">Add to Cart</button>
</actions.addItem.Form>
</>
);
}
π Tracking Submission Stateβ
import { useJodsFetchers } from "jods/remix";
function SubmitButton() {
const { isSubmitting } = useJodsFetchers("cart.addItem");
return (
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Adding..." : "Add to Cart"}
</button>
);
}
π Form Transition Statesβ
import { useJodsTransition } from "jods/remix";
function FormStatus() {
const { isPending } = useJodsTransition("user.updateProfile");
if (isPending) {
return <div className="status">Saving changes...</div>;
}
return null;
}
π΄ Individual Hooks (Legacy Approach)β
While useJods is the recommended way to access jods data and actions, you can still use the individual hooks for specialized cases:
// Legacy approach with separate hooks
import { useJodsStore, useJodsForm } from "jods/remix";
import { user } from "~/jods/user.jods";
export default function ProfileLegacy() {
const userData = useJodsStore(user);
const form = useJodsForm(user, "updateProfile");
return (
<div>
<h1>Profile</h1>
<form.Form>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" defaultValue={userData.name} />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" defaultValue={userData.email} />
</div>
<button type="submit">Update Profile</button>
</form.Form>
</div>
);
}
β¨ Key Featuresβ
- π Server-Client Synchronization: State is automatically hydrated from server to client
- π Form Handling: Built-in form utilities with validation
- π‘οΈ Type Safety: Full TypeScript and schema support with
j/jodaliases - β‘ Optimistic Updates: Manage pending state with useJodsFetchers
- βοΈ Direct Mutations: Update your state naturally with direct property assignments
π Privacy Considerationsβ
When using jods with Remix, it's important to understand how data flows between server and client:
// app/jods/user.jods.ts
import { defineStore } from "jods/remix";
import { j } from "jods/zod";
export const user = defineStore({
name: "user",
schema: j.object({
name: j.string(),
email: j.string().email(),
// Sensitive data that shouldn't be exposed to client
role: j.string(),
securityKey: j.string().optional(),
}),
// ...
});
By default, all store data loaded on the server is hydrated to the client. This means any sensitive data in your store will be available in client-side JavaScript.
Handling Sensitive Dataβ
To protect sensitive information:
- Server-only properties: Consider using a separate store for server-only data
- Data filtering: Filter sensitive data before returning it from loaders
- Use jods persist (coming in future release, see issue #57): Will allow specifying which properties should be persisted to client
// Example: Filtering sensitive data in loader
loader: async ({ request }) => {
const userData = await getUserData();
// Don't send sensitive fields to client
const { securityKey, internalNotes, ...safeUserData } = userData;
return safeUserData;
};
π API Referenceβ
π useJodsβ
The recommended unified hook for accessing both store data and actions. Read more
π¦ defineStoreβ
Creates a jods store with server-side handlers and loaders. Read more
π withJodsβ
Higher-order function to integrate jods with Remix πΏ loaders and actions. Read more
π§ rehydrateClientβ
Component to rehydrate jods stores on the client from server state. Read more
πͺ useJodsStoreβ
React hook to access the current state of a jods store. Read more
π useJodsFormβ
React hook to create form bindings for a jods store action. Read more
π useJodsFetchersβ
React hook to track the state of all fetchers for a specific jods store action. Read more
π¦ useJodsTransitionβ
React hook to track transition state for jods action submissions. Read more
π useOptimisticUpdateβ
React hook for implementing optimistic UI updates with jods stores. Read more