πΏ πΏοΈ π¦ 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
useJods
hook 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
/jod
aliases - β‘ 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