Framework Integrations Guide
This guide documents how jods integrates with popular frontend frameworks and provides best practices for using these integrations.
Framework Integration Architecture
jods provides first-class support for several frontend frameworks through dedicated integration modules:
src/
├── frameworks/ # Framework integrations
│ ├── react/ # React integration
│ ├── preact/ # Preact integration
│ └── remix/ # Remix integration
│
└── utils/ # Utility functions
Each framework integration follows a consistent pattern:
- Entry point:
frameworks/{framework}/index.ts
exports all integration features - Hooks: Framework-specific hooks are located within their respective
src/frameworks/{framework}/
directories (e.g.,useJods.ts
,useJodsPreact.ts
). - Additional utilities: Framework-specific utilities in their respective directories
React Integration
React integration provides a seamless way to use jods stores in React components.
Core Features
- useJods hook: Automatically subscribes to store changes and updates components
- Computed value resolution: Automatically resolves computed properties when accessed
- React lifecycle integration: Handles subscriptions with proper lifecycle methods
Usage Example
import { store } from "jods";
import { useJods } from "jods/react";
// Create a store
const userStore = store({
firstName: "John",
lastName: "Doe",
fullName: computed(() => `${userStore.firstName} ${userStore.lastName}`),
});
// Use in a component
function UserProfile() {
const user = useJods(userStore);
return (
<div>
<h1>{user.fullName}</h1> {/* Computed property auto-resolved */}
<input
value={user.firstName}
onChange={(e) => (user.firstName = e.target.value)}
/>
</div>
);
}
Type Safety
When using the useJods
hook with TypeScript, ensure proper type annotations:
interface UserStore {
firstName: string;
lastName: string;
fullName: string; // Type representing the computed value result
}
// Properly type the useJods hook
const user = useJods<UserStore>(userStore);
When rendering computed values, it's recommended to ensure they're the expected type:
// Explicit conversion to string for computed values
return <div>{String(user.fullName)}</div>;
Enhanced Type Safety Patterns
For even better type safety with the useJods
hook, follow these patterns:
-
Explicit Type Imports
Import React types explicitly for maximum clarity:
import type { Dispatch, SetStateAction } from "react";
-
Explicit Type Assertions for State Management
When defining state with
useState
, use explicit type assertions:// Properly typed with explicit type assertion
const [state, setState] = useState(() => store.getState()) as [
T,
Dispatch<SetStateAction<T>>
]; -
Thorough Type Checking for Computed Values
When handling computed values in proxies, use comprehensive type checking:
get(obj, prop) {
const value = Reflect.get(obj, prop);
// Thorough type checking before resolving computed value
if (value && typeof value === "function" && isComputed(value)) {
return (value as any)();
}
return value;
} -
Type Guards for Computed Values
Create type guards to safely check for computed values:
function isComputedFunction<T>(
value: unknown
): value is (() => T) & { __computed: true } {
return (
!!value &&
typeof value === "function" &&
(value as any).__computed === true
);
}
// Then use the guard
if (isComputedFunction<string>(value)) {
return value();
}
Preact Integration
Preact integration follows a similar pattern to React but is optimized for Preact's specific hooks API.
Core Features
- useJods hook: Similar to React but using Preact's hooks
- Smaller bundle size: Optimized for Preact's smaller footprint
- Equivalent API: Same API as React integration for consistency
Usage Example
import { store } from "jods";
import { useJods } from "jods/preact";
// Create a store
const counterStore = store({ count: 0 });
// Use in a Preact component
function Counter() {
const state = useJods(counterStore);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => counterStore.count++}>Increment</button>
</div>
);
}
Remix Integration
Remix integration provides more complex features for server-side rendering support.
Core Features
- defineStore: Creates a named store that can be registered with the Remix system
- useJods: Unified hook combining store state with form handlers
- useJodsStore: Provides reactive state updates with property tracking
- useJodsForm: Creates form components that connect to store handlers
- Server-side rendering: Supports hydration and dehydration of state
Usage Example
// Define a store
import { defineStore } from "jods/remix";
import { j } from "jods";
const todoStore = defineStore({
name: "todos",
schema: j.object({
items: j.array(
j.object({
id: j.string(),
text: j.string(),
completed: j.boolean(),
})
),
}),
initialState: {
items: [],
},
handlers: {
// Action handlers
addTodo: (store, { text }) => {
store.items.push({
id: Math.random().toString(36).substr(2, 9),
text,
completed: false,
});
},
toggleTodo: (store, { id }) => {
const todo = store.items.find((item) => item.id === id);
if (todo) todo.completed = !todo.completed;
},
},
});
// Use in a Remix component
import { useJods } from "jods/remix";
export default function TodoApp() {
const { stores, actions } = useJods(todoStore, ["addTodo", "toggleTodo"]);
return (
<div>
<h1>Todo List ({stores.items.length})</h1>
<actions.addTodo.Form>
<input name="text" placeholder="Add todo" />
<button type="submit">Add</button>
</actions.addTodo.Form>
<ul>
{stores.items.map((todo) => (
<li key={todo.id}>
<actions.toggleTodo.Form>
<input type="hidden" name="id" value={todo.id} />
<button type="submit">
{todo.completed ? "✓" : "○"} {todo.text}
</button>
</actions.toggleTodo.Form>
</li>
))}
</ul>
</div>
);
}
AI-Optimized Framework Hooks
jods provides AI-optimized versions of framework hooks to improve context window efficiency when using AI assistants to work with the codebase.
Available AI-Optimized Files
src/ai/react-useJods.ai.ts
: Simplified version of React hooksrc/ai/preact-useJods.ai.ts
: Simplified version of Preact hooksrc/ai/remix-useJods.ai.tsx
: Simplified version of Remix hooksrc/ai/remix-useJodsStore.ai.tsx
: Simplified version of Remix store hooksrc/ai/remix-useJodsForm.ai.tsx
: Simplified version of Remix form hook
Key Differences in AI-Optimized Files
- Simplified implementations: Less code with the same functionality
- Removal of test-specific code: Clean production-focused code
- Consistent debug utilities: Using the
debug
utility instead of console.log - More concise comments: Focusing on "why" not "what"
When to Reference AI-Optimized Files
- For understanding core concepts: AI-optimized files are easier to understand
- When working with AI assistants: Reduces token usage for better responses
- Learning the codebase: Clearer picture of how integrations work
The standard implementation files should be referenced for:
- Implementation details that might be simplified in AI-optimized versions
- Test-specific behavior and edge cases
- Complete API surface area
Common Integration Patterns
When working with framework integrations, you'll observe these common patterns:
1. Framework Detection
// Detecting if running in a specific framework context
function isReactContext() {
return (
(typeof window !== "undefined" && !!(window as any).React) ||
(typeof globalThis !== "undefined" && !!(globalThis as any).React)
);
}
// Using framework detection
if (isReactContext()) {
// Handle React-specific behavior
}
2. Hook Patterns
All framework hooks follow a consistent pattern:
function useFrameworkHook(store) {
// 1. Initialize state with current store state
const [state, setState] = useState(() => store.getState());
// 2. Subscribe to store changes on mount
useEffect(() => {
const unsubscribe = store.subscribe((newState) => {
setState(newState);
});
// 3. Unsubscribe on unmount
return unsubscribe;
}, [store]);
// 4. Return state or enhanced state
return state;
}
3. Computed Value Resolution
Automatic resolution of computed values using proxies:
// Create a proxy to auto-resolve computed values
const proxiedState = new Proxy(state, {
get(obj, prop) {
const value = Reflect.get(obj, prop);
// If property is a computed value, call it
if (isComputed(value)) {
return value();
}
return value;
},
});
4. Debug Utilities
Framework integration code uses the debug utility with framework-specific categories:
import { debug } from "../utils/debug";
// React-specific logging
debug.log("react", "Setting up store subscription");
// Preact-specific logging
debug.log("preact", "Store changed, updating component");
// Remix-specific logging
debug.log("remix", "Creating form for handler: " + handler);
Best Practices
When working with framework integrations:
-
Use the appropriate hook for your framework:
- React:
import { useJods } from 'jods/react'
- Preact:
import { useJods } from 'jods/preact'
- Remix:
import { useJods } from 'jods/remix'
- React:
-
Handle type safety explicitly:
- Provide generic type parameters to
useJods<T>
- Convert computed values to appropriate types before rendering
- Use TypeScript interfaces to document expected store structure
- Provide generic type parameters to
-
Maintain framework-specific patterns:
- Follow React's rules of hooks when working with the React integration
- Use Preact's specific imports (
preact/hooks
) for Preact integration - Follow Remix conventions for Remix integration
-
Debug efficiently with debug utility:
- Enable debug for specific framework categories as needed
- Use consistent debug message formats
- Provide meaningful context in debug messages
Troubleshooting
Common issues when working with framework integrations:
-
Component not updating when store changes:
- Check that you're using the
useJods
hook, not accessing the store directly - Verify the component is actually re-rendering when expected
- Confirm you're not creating a new store on each render
- Check that you're using the
-
Type errors with computed values:
- Ensure computed values are properly typed
- Use explicit type conversions when rendering (
String()
, etc.) - Define proper TypeScript interfaces for your stores
-
React DevTools showing incorrect values:
- This is expected behavior for proxied values
- The store itself contains the canonical state
- Use the jods debugger for more accurate debugging
-
Server/client hydration issues in Remix:
- Ensure proper hydration by using
withJods
in loaders - Check that your store definition is consistent between server and client
- Use the rehydration utilities provided by jods/remix
- Ensure proper hydration by using