Skip to main content

Remix SSR

This recipe shows how to implement gates in a Remix application using the server-side rendering (SSR) pattern, with efficient server and client-side handling.

Server Setup

First, set up the Gates service on the server: app/utils/gatesClient.server.ts

import { Gates } from "@withgates/node";
import { getSession } from "./session.server";

class GatesService {
private gates: Gates;
private initialized = false;

constructor() {
this.gates = new Gates(process.env.GATE_PUBLIC_KEY!);
}

async initialize(request: Request) {
if (!this.initialized) {
await this.gates.init();
this.initialized = true;
} else {
return this.gates;
}

const session = await getSession(request);
const userId = session.get("userId");

if (userId) {
await this.gates.signInUser(userId);
}

return this.gates;
}

getFlags() {
return {
knobs: this.gates.store?.knobs || {},
experiments: this.gates.store?.experiments || {},
};
}

async sync() {
return this.gates.sync();
}
}

// Single instance that persists between requests
const gatesService = new GatesService();

export { gatesService };

Root Setup

Initialize gates and make them available throughout the app: app/root.tsx

import { json, type LoaderFunctionArgs } from "@remix-run/node";

export async function loader({ request }: LoaderFunctionArgs) {
const { gatesService } = await import("~/utils/gatesClient.server");

await gatesService.initialize(request);

return json({
gates: gatesService.getFlags(),
});
}

export function Layout({ children }: { children: React.ReactNode }) {
const { gates } = useLoaderData<typeof loader>();

return (
<html lang="en">
<head>
{/* ... other head elements ... */}
</head>
<body>
<script
dangerouslySetInnerHTML={{
__html: `window.INITIAL_GATES = ${JSON.stringify(gates)};`,
}}
/>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

Client Hook

Create a hook to access feature flags throughout your app: app/hooks/useGates.ts

import { useRouteLoaderData } from "@remix-run/react";
import pkg from "crypto-js";
const { MD5 } = pkg;

type GatesStore = {
knobs: Record<string, boolean>;
experiments: Record<string, boolean>;
};

type RootLoaderData = {
gates: GatesStore;
};

function createHash(value: string) {
return MD5(value).toString().slice(0, 6);
}

export const useGates = () => {
const root = useRouteLoaderData<RootLoaderData>("root");
const flags = root?.gates || { knobs: {}, experiments: {} };

return {
isEnabled: (key: string): boolean => {
const hash = createHash(key);
return flags.knobs[hash] ?? false;
},
isInExperiment: (key: string): boolean => {
const hash = createHash(key);
return flags.experiments[hash] ?? false;
},
};
};

Usage in Components

Use the feature flags in your components: app/routes/dashboard/knobs.tsx

import { useGates } from "~/hooks/useGates";

export default function Knobs() {
const { isEnabled } = useGates();

// Check if a feature is enabled
const hasAdminFilters = isEnabled("admin_filters");

return (
<div>
{hasAdminFilters && (
<div className="filters">
{/* Admin filter UI */}
</div>
)}
</div>
);
}

Key Benefits

  1. Efficient Data Flow

    • Uses Remix's built-in data loading patterns
    • Avoids unnecessary re-renders
    • Maintains server-client consistency
  2. Performance

    • Single instance of gates service on server
    • Client-side feature checking without additional requests
    • Efficient hash-based lookups
  3. Type Safety

    • Full TypeScript support
    • Clear interfaces for gates data
    • Compile-time error checking
  4. Developer Experience

    • Simple hook-based API
    • Consistent feature flag checking
    • Easy to maintain and extend

Notes

  • The gates service is initialized once on the server and persists between requests
  • Feature flags are injected into the client via window.INITIAL_GATES
  • The useGates hook provides a clean API for checking features
  • Hash generation is consistent between server and client
  • No style flickering during navigation due to stable data patterns

This recipe provides a solid foundation for gates in Remix applications while maintaining performance and developer experience.