Skip to main content

πŸ’Ώ 🐿️ πŸ¦† 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​

  1. Create a jods directory in your app root
  2. Define your stores using defineStore
  3. 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(),
}),
// ...
});
Security Warning

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:

  1. Server-only properties: Consider using a separate store for server-only data
  2. Data filtering: Filter sensitive data before returning it from loaders
  3. 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