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
-
Efficient Data Flow
- Uses Remix's built-in data loading patterns
- Avoids unnecessary re-renders
- Maintains server-client consistency
-
Performance
- Single instance of gates service on server
- Client-side feature checking without additional requests
- Efficient hash-based lookups
-
Type Safety
- Full TypeScript support
- Clear interfaces for gates data
- Compile-time error checking
-
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.