SSR State Managements / Singleton class #1926
Replies: 2 comments
-
1. Unnecessary class construction issue on the client
// 🔍 MyService.ts file
class MyService {
static instance: MyService;
state: string | null = null;
constructor() {
console.log('🏗️ MyService constructor called!'); // This gets executed
if (MyService.instance) {
return MyService.instance;
}
MyService.instance = this;
this.state = new Date().toJSON();
}
}
// ⚠️ Constructor is executed here immediately!
export default new MyService(); // <- This line causes the issue!
// 🔍 In another file
import MyService from './MyService'; // <- Constructor executes at this moment!
const getServerTime = async () => {
'use server';
// Here we're not "calling" MyService, just referencing the already created instance
console.log(MyService); // Prints the already created instance
return MyService.state; // Returns state from the already created instance
}; Execution Order:
Methods to delay constructor execution: // Method 1: Export only the class
class LazyMyService {
state: string | null = null;
constructor() {
console.log('🏗️ LazyMyService constructor called!');
this.state = new Date().toJSON();
}
}
// Don't export an instance
export { LazyMyService };
// Create only when used
import { LazyMyService } from './LazyMyService';
const getLazyServerTime = async () => {
'use server';
// Constructor executes only now
const service = new LazyMyService();
return service.state;
};
// Method 2: Use factory function
class FactoryMyService {
state: string | null = null;
constructor() {
console.log('🏗️ FactoryMyService constructor called!');
this.state = new Date().toJSON();
}
}
let cachedInstance: FactoryMyService | null = null;
export function getMyServiceInstance() {
if (!cachedInstance) {
console.log('Creating new instance...');
cachedInstance = new FactoryMyService();
}
return cachedInstance;
}
const getFactoryServerTime = async () => {
'use server';
// Created only when needed
const service = getMyServiceInstance();
return service.state;
};
// Method 3: Use `globalThis`
class GlobalMyService {
state: string | null = null;
constructor() {
console.log('🏗️ GlobalMyService constructor called!');
this.state = new Date().toJSON();
}
}
function getGlobalMyService() {
const key = '__globalMyService';
if (typeof globalThis !== 'undefined') {
if (!globalThis[key]) {
console.log('Creating global instance...');
globalThis[key] = new GlobalMyService();
}
return globalThis[key] as GlobalMyService;
}
// Client fallback
return new GlobalMyService();
}
export const getGlobalServerTime = async () => {
'use server';
const service = getGlobalMyService();
return service.state;
};
// Method 4: Module-level variables
let moduleState = {
timestamp: null as string | null,
initialized: false
};
function initializeState() {
if (!moduleState.initialized) {
console.log('🏗️ Initializing module state...');
moduleState.timestamp = new Date().toJSON();
moduleState.initialized = true;
}
}
export const getModuleServerTime = async () => {
'use server';
initializeState(); // Initialize only when needed
return moduleState.timestamp;
};
// Method 5: Use BroadcastChannel
// See the reference: https://www.answeroverflow.com/m/1318980581849038921
// Method 6: Save and load to separate storage, such as a file system or database
// I'll skip the example itself here, as the settings will vary depending on your environment.
// 🧪 Test to see when constructor executes
/*
You can verify when the constructor executes with this code:
// test-import.ts
console.log('Before import');
import MyService from './MyService'; // ✅ The MyService module has already been loaded and evaluated.
console.log('After import');
const testFunction = () => {
console.log('In test function');
console.log(MyService.state); // Already created state
};
*/ This solves object creation on the client, but half of the methods will still create objects twice on the server (on render, and the first time you call the server function) Therefore, to solve the second issue, you must use one of the methods 3, 5, or 6. 2. Different Server Context between SSR Context and Server Function ContextReferences:
Therefore, in this case, you should use a global store, such as Method 3 or 6(Method 6 only connects with external storage on the first server function call). You can also manage global variables using the |
Beta Was this translation helpful? Give feedback.
-
There is an old issue or discussion regarding this problem, but I can't find it right now. The suggested fix I'm using is to simply assign the instance to type Database = PostgresJsDatabase<typeof schema> & { $client: postgres.Sql };
declare global {
var db: Database;
}
const createClient = (): Database => {
const connection = process.env.DATABASE_URL;
if (!connection) throw Error("Missing DATABASE_URL");
const url = new URL(connection);
log.info("Connecting to", url.hostname, url.pathname.slice(1));
return drizzle({ connection, schema, casing: "snake_case" });
};
const init = (): Database => {
globalThis.db ??= createClient();
return globalThis.db;
};
export const db = init(); |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Hi everyone,
I have a class that I want to initialize only once on the server side, and then use this single instance throughout the server's lifetime.
However, I’m encountering an issue: two instances of the class are being created - one during the initial render on the server, and another for subsequent uses.
For example, reloading the page accesses the first instance, but refetching accesses the second.
Here is a minimal example demonstrating the issue:
https://stackblitz.com/edit/github-gafckbo7?file=src%2Fcomponents%2FCounter.tsx
Am I missing something?
Is it reasonable to use a class for state management on the server side in this way?
Are there better options for managing server-side state?
Thank you!
Beta Was this translation helpful? Give feedback.
All reactions