Skip to main content

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:

  1. Entry point: frameworks/{framework}/index.ts exports all integration features
  2. Hooks: Framework-specific hooks are located within their respective src/frameworks/{framework}/ directories (e.g., useJods.ts, useJodsPreact.ts).
  3. 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:

  1. Explicit Type Imports

    Import React types explicitly for maximum clarity:

    import type { Dispatch, SetStateAction } from "react";
  2. 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>>
    ];
  3. 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;
    }
  4. 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 hook
  • src/ai/preact-useJods.ai.ts: Simplified version of Preact hook
  • src/ai/remix-useJods.ai.tsx: Simplified version of Remix hook
  • src/ai/remix-useJodsStore.ai.tsx: Simplified version of Remix store hook
  • src/ai/remix-useJodsForm.ai.tsx: Simplified version of Remix form hook

Key Differences in AI-Optimized Files​

  1. Simplified implementations: Less code with the same functionality
  2. Removal of test-specific code: Clean production-focused code
  3. Consistent debug utilities: Using the debug utility instead of console.log
  4. 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:

  1. 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'
  2. 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
  3. 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
  4. 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:

  1. 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
  2. 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
  3. 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
  4. 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