Create sync Realm outside React flow

Hi!

I saw in the Realm React documentation that the createRealmContext can accept a Realm instance rather than a config object which is what I need for my app.
The problem is that my Realm is a synced one which means I need a user in order for my synced Realm to open but on first app launch the user hasn’t got a chance to log in.
Here’s a code sample:

// This code throws because `user` is not defined in the sync config
const {
    RealmProvider: SyncRealmProvider,
    useObject: useSyncObject,
    useQuery: useSyncQuery,
    useRealm: useSyncRealm,
} = createRealmContext({
    schema: [...],
    sync: {
        flexible: true,
        initialSubscriptions: {
            update: (subs, realm) => {...},
        },
        newRealmFileBehavior: { type: OpenRealmBehaviorType.OpenImmediately },
        existingRealmFileBehavior: { type: OpenRealmBehaviorType.OpenImmediately },
        clientReset: {
            mode: ClientResetMode.RecoverOrDiscardUnsyncedChanges,
            onFallback: () => console.log("recovery"),
        },
    },
})

const App = () => {
    return (
        <AppProvider id={appId}>
             <UserProvider fallback={Login}>
                 <SyncRealmProvider fallback={<SplashScreen />}>
                    ...
    )
}

As you can see in the code above, the Realm is created before the React code runs, which means the fallback for the UserProvider component hasn’t had a chance to run yet and the user has not been redirect to the login page.

I would like to know what is the correct way to handle this case using a synced Realm. Should I create more than one Relam instance?

Cheers,

Hey Renaud,

You’re going about this totally wrong, here’s a better way to go for this:

  1. Initialize the Realm Context After User Authentication

Instead of creating the Realm context immediately, you should create it dynamically, after the user is authenticated. This ensures that the user is available when you configure the synced Realm. This will also eliminate your situation almost entirely… Then

  1. Use a State or Effect to Manage Realm Context Creation

You can use a useState or useEffect to manage the creation of the Realm context dynamically after the user has been authenticated.

It would look something like this:

import React, { useState, useEffect } from 'react';
import { AppProvider, UserProvider, useUser } from '@realm/react';
import { createRealmContext } from '@realm/react';
import { Login, SplashScreen } from './components';

This is more in line with the direction that you want to go for something like this. I had a client years back who had a similar problem with Swift SDK and by changing it to initiate AFTER authentication, will make it all work flawlessly.

But that said, if you TRULY have to have Realm already running before being logged in, you can simply just use a second Realm instance as you can use as many as you want in the same app for whatever reasons. Just simply replicate your Realm and put in your second Realm App info and set it to auto authenticate. And then the secured portion where you want someone to login to access, you just do it like you normally would.

But above is a lot simpler to navigate this without the added complications.

Hi @Brock_Leonard,
Thank you for your reply.

I ended up doing something similar to what you propose but it feels a bit hacky I wanted to make sure there wasn’t a “cleaner” way.
Since I need to access the Realm instance outside of React I ended up doing something like

// UserProvider is the direct parent of this component
let realmMethods;

const CustomRealmProvider = ({ children }) => {
    const user = useUser();
    useEffect(() => {
        realm = createRealmContext({...})
    }, [user])

    const RealmProvider = realmMethods?.RealmProvider

    return (
        RealmProvider && <RealmProvider>{children}</RealmProvider>
    )
}

PS: for those looking for another way, you can also use the same config and open another instance of the same Realm as long as the call is made AFTER UserProvider has set a user

const config = {...}
const methods = createRealmContext(config)

const myFunctionOutsideReact = () => {
    Realm.open(config)
}

// Child of UserProvider
const MyComponent = () => {
    useEffect(() => {
        myFunctionOutsideReact()
    }, [])
}

That’s somewhat similar to the solutions I created in Swift. The first approach is as I described earlier. The second approach involves using a local Realm without requiring sign-in initially, then transitioning to Realm Sync once the user signs in. Both of these solutions are functional and have been tested. I’ve sanitized the examples, so if there are any discrepancies, please let me know. However, these should provide a good direction to follow.

Here are the Swift solutions I got in my repos it would need to be translated to JavaScript (React) specifically like I did above for the imports you want to have.

But this is the direction that would help you out, and yes I have tested this in a demo app. It does work just fine on an M3 Pro Mac mini.

Keep in mind, this is written in Swift, and you’d have to translate the React Equivalent but Swift is pretty easy to read for the direction it’s going as a solution for what you should try to do.

import SwiftUI
import RealmSwift

// Define your App ID
let appId = "your-app-id"
let app = App(id: appId)

// Your Realm context provider setup
let realmConfig: Realm.Configuration = {
    let user = app.currentUser!
    var config = user.flexibleSyncConfiguration(initialSubscriptions: { subs in
        // Set up your initial subscriptions here
    })
    config.objectTypes = [YourObjectType.self]  // Replace with your object types
    return config
}()

// Your login view
struct LoginView: View {
    @State private var email: String = ""
    @State private var password: String = ""
    @State private var isLoggingIn = false

    var body: some View {
        VStack {
            TextField("Email", text: $email)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            
            SecureField("Password", text: $password)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()

            if isLoggingIn {
                ProgressView()
            } else {
                Button("Log In") {
                    isLoggingIn = true
                    app.login(credentials: .emailPassword(email: email, password: password)) { result in
                        switch result {
                        case .success(let user):
                            print("Successfully logged in as user: \(user)")
                            // Here the user is logged in, and we can now initialize the Realm
                            NotificationCenter.default.post(name: NSNotification.Name("UserDidLogin"), object: nil)
                        case .failure(let error):
                            print("Failed to log in: \(error.localizedDescription)")
                        }
                        isLoggingIn = false
                    }
                }
            }
        }
        .padding()
    }
}

// Your main app view
struct ContentView: View {
    @State private var isLoggedIn = false

    var body: some View {
        if isLoggedIn {
            SyncRealmView()
        } else {
            LoginView()
                .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("UserDidLogin"))) { _ in
                    self.isLoggedIn = true
                }
        }
    }
}

// The view where you use the synced Realm
struct SyncRealmView: View {
    @ObservedResults(YourObjectType.self, configuration: realmConfig) var objects

    var body: some View {
        List(objects) { object in
            Text("\(object.name)")
        }
    }
}

@main
struct YourApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

To explain what it’s doing:

  • The LoginView handles user authentication. Once the user successfully logs in, a notification (UserDidLogin ) is posted.
  • The ContentView listens for the UserDidLogin notification. Once the notification is received, it updates the state to show the SyncRealmView that uses the synced Realm instance.
  • The realmConfig is defined based on the current user. It’s set up after the user has logged in, ensuring that the Realm can be opened with a valid user.

Now, there is ANOTHER way to do this, and this is the two Realms method I mentioned, I don’t have examples in JavaScript, but I do have working examples of this in Swift already I’ve sanitized.

import SwiftUI
import RealmSwift

// Define your App ID
let appId = "your-app-id"
let app = App(id: appId)

// Local Realm configuration (Unauthenticated state)
let localConfig = Realm.Configuration(inMemoryIdentifier: "localRealm")

// Synced Realm configuration (Authenticated state)
func syncedRealmConfig(for user: User) -> Realm.Configuration {
    var config = user.flexibleSyncConfiguration(initialSubscriptions: { subs in
        // Set up your initial subscriptions here
    })
    config.objectTypes = [YourObjectType.self]  // Replace with your object types
    return config
}

// Your login view
struct LoginView: View {
    @State private var email: String = ""
    @State private var password: String = ""
    @State private var isLoggingIn = false

    var body: some View {
        VStack {
            TextField("Email", text: $email)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            
            SecureField("Password", text: $password)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()

            if isLoggingIn {
                ProgressView()
            } else {
                Button("Log In") {
                    isLoggingIn = true
                    app.login(credentials: .emailPassword(email: email, password: password)) { result in
                        switch result {
                        case .success(let user):
                            print("Successfully logged in as user: \(user)")
                            // Switch to synced Realm after login
                            NotificationCenter.default.post(name: NSNotification.Name("UserDidLogin"), object: user)
                        case .failure(let error):
                            print("Failed to log in: \(error.localizedDescription)")
                        }
                        isLoggingIn = false
                    }
                }
            }
        }
        .padding()
    }
}

// Your main app view
struct ContentView: View {
    @State private var realmConfig: Realm.Configuration = localConfig
    @State private var isLoggedIn = false

    var body: some View {
        if isLoggedIn {
            SyncedRealmView(realmConfig: realmConfig)
        } else {
            LocalRealmView(realmConfig: realmConfig)
                .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("UserDidLogin"))) { notification in
                    if let user = notification.object as? User {
                        // Switch to synced Realm configuration
                        self.realmConfig = syncedRealmConfig(for: user)
                        self.isLoggedIn = true
                    }
                }
        }
    }
}

// View using the local Realm
struct LocalRealmView: View {
    let realmConfig: Realm.Configuration
    
    var body: some View {
        // Replace `YourLocalObjectType` with your local Realm object type
        @ObservedResults(YourLocalObjectType.self, configuration: realmConfig) var objects
        
        List(objects) { object in
            Text("\(object.name)")
        }
    }
}

// View using the synced Realm
struct SyncedRealmView: View {
    let realmConfig: Realm.Configuration
    
    var body: some View {
        // Replace `YourObjectType` with your synced Realm object type
        @ObservedResults(YourObjectType.self, configuration: realmConfig) var objects
        
        List(objects) { object in
            Text("\(object.name)")
        }
    }
}

@main
struct YourApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

What it’s doing:

  • We have a LOCAL Realm that’s used to access local data without login.
  • Synced Realm after logging in.

To better clarify:
You make the app start with a local in memory Realm configuration (localConfig ) when the user is not logged in. Then you manage the local only data while the user is unauthenticated.

Once the user logs in, the app switches to a synced Realm configuration. This is done by listening for a login notification (UserDidLogin ) and updating the realmConfig in ContentView to point to the synced Realm.

Keep in mind LocalRealmView and SyncedRealmView are two different views that each use their respective Realm configurations. LocalRealmView uses the local Realm, and SyncedRealmView uses the synced Realm after login.

You have to make sure the app listens for a notification that the user has logged in. Once this happens it updates the state to use the synced Realm configuration.

This is probably more so the direction you’re wanting to go for, but both of these solutions work for what you’re wanting to do. But the better route is to have it wait until after logging in.

Hey Renaud,

As part of a Device Sync exercise, a couple of my engineers reviewed your problem and proposed the following solution (special credit to Charlotte Hughes and Kayla Green). Their approach involves turning Realm into a singleton service and then creating a custom hook to provide Realm information in a more React-friendly way. Additionally, they developed another custom hook to serve as a provider for the rest of the app.

Unlike my examples above, this is all in React.

Step 1: Creating the Singleton Service for Realm (RealmService)

  • Purpose: The RealmService is designed to manage a single instance of the Realm database throughout your application. By making Realm a singleton, you ensure that only one instance of the Realm database is created and used consistently across different parts of your app.

How it Works:

  • Initialization: The service has an initializeRealm method that initializes Realm with the provided configuration. If an instance already exists, it returns that instance instead of creating a new one.
  • Access: The getRealmInstance method allows any part of your app to access the initialized Realm instance. If the Realm instance hasn’t been initialized yet, it throws an error.
  • Cleanup: The closeRealm method can be used to close the Realm instance when it’s no longer needed, such as when the user logs out or the app is shutting down.

Step 2: Creating the Custom Hook (useRealm)

  • Purpose: The useRealm hook ties the lifecycle of the Realm instance to the React component lifecycle. It initializes Realm when the user is set and provides the Realm instance to the component.

How it Works:

  • User Dependency: The hook takes user and config as dependencies. Whenever the user changes (e.g., a user logs in or out), the hook re-initializes Realm with the new configuration.
  • Effect Hook: Inside the useEffect hook, the initializeRealm method from RealmService is called to create or retrieve the Realm instance. The hook also handles cleanup by closing the Realm instance when the component unmounts or when the user changes.
  • Return Value: The hook returns the initialized Realm instance, which can then be used within the component.

Step 3: Creating the RealmProvider Component

  • Purpose: The RealmProvider component uses the useRealm hook to provide the Realm instance to the entire component tree via React’s context API. This makes the Realm instance accessible to any component within the provider’s tree.

How it Works:

  • Context Creation: The RealmContext is created using React’s createContext method, which will hold the Realm instance.
  • Providing Realm: The RealmProvider component calls the useRealm hook to get the Realm instance and then passes it down through the RealmContext.Provider.
  • Conditional Rendering: The RealmProvider only renders its children if the Realm instance is available. This ensures that the children components don’t try to access Realm before it’s initialized.

Step 4: Using the RealmProvider in Your Application

  • Purpose: The RealmProvider is used at a higher level in your application, wrapping any components that need access to the Realm instance.

How it Works:

  • App Component: In your App component, the RealmProvider is wrapped around other components. The user and config are passed as props to the RealmProvider, ensuring that the Realm instance is initialized with the correct configuration for the current user.
  • Accessing Realm: Inside any child component of RealmProvider, the useRealmContext hook can be used to access the Realm instance, making it easy to interact with Realm data.

Step 5: Accessing Realm Outside of React

  • Purpose: The RealmService is also accessible outside of React components, allowing you to interact with the Realm instance in non-React code (e.g., utility functions, services, or Node.js scripts).

How it Works:

  • Direct Access: By calling RealmService.getRealmInstance(), you can retrieve the initialized Realm instance outside of React components. This is particularly useful for operations that need to interact with Realm but are not part of the React component tree.

Summary of the Workflow

  1. Initialization: RealmService manages the initialization and lifecycle of the Realm instance, ensuring a single, consistent instance across the app.
  2. React Integration: The useRealm hook integrates this singleton instance into React components, managing it based on the user state.
  3. Context Provider: RealmProvider uses this hook to make the Realm instance available to any component within its tree.
  4. Outside Access: RealmService provides a way to access the same Realm instance outside of the React component tree.

RealmService.JS

import * as Realm from "realm";

let realmInstance = null;

const RealmService = {
    initializeRealm(config) {
        if (!realmInstance) {
            realmInstance = new Realm(config);
        }
        return realmInstance;
    },

    getRealmInstance() {
        if (!realmInstance) {
            throw new Error("Realm instance has not been initialized yet!");
        }
        return realmInstance;
    },

    closeRealm() {
        if (realmInstance && !realmInstance.isClosed) {
            realmInstance.close();
            realmInstance = null;
        }
    }
};

export default RealmService;

useRealm.js

import { useState, useEffect } from "react";
import RealmService from "./RealmService";

const useRealm = (user, config) => {
    const [realm, setRealm] = useState(null);

    useEffect(() => {
        if (user) {
            const initializedRealm = RealmService.initializeRealm(config);
            setRealm(initializedRealm);

            return () => {
                RealmService.closeRealm();
            };
        }
    }, [user, config]);

    return realm;
};

export default useRealm;

RealmProvider.js

import React, { createContext, useContext } from "react";
import useRealm from "./useRealm";

const RealmContext = createContext();

export const RealmProvider = ({ user, config, children }) => {
    const realm = useRealm(user, config);

    return (
        <RealmContext.Provider value={realm}>
            {realm ? children : null}
        </RealmContext.Provider>
    );
};

export const useRealmContext = () => {
    return useContext(RealmContext);
};

App.js

import React from "react";
import { RealmProvider } from "./RealmProvider";
import { useUser } from "./UserProvider";  // Assuming you have a UserProvider

const App = () => {
    const user = useUser();
    const realmConfig = {
        // Define your Realm configuration here
    };

    return (
        <RealmProvider user={user} config={realmConfig}>
            <MyComponent />
        </RealmProvider>
    );
};

const MyComponent = () => {
    const realm = useRealmContext();

    // Use realm for your app's logic
    // Example: Accessing data from Realm
    return <div>{realm && "Realm is ready!"}</div>;
};

export default App;

OutsideReact.js

import RealmService from "./RealmService";

const myFunctionOutsideReact = () => {
    try {
        const realm = RealmService.getRealmInstance();
        // Perform operations with the realm instance
    } catch (error) {
        console.error("Realm is not initialized: ", error);
    }
};

'

Hi @Brock_Leonard!

Thank you so much for this very thorough answer it helped me a lot.
In case anyone is looking for ideas here’s what I did

const syncRealmConfig = {...};

class RealmSingleton extends Realm {
    static #instance = null;

    constructor(config) {
        if (RealmSingleton.#instance) {
            return RealmSingleton.#instance;
        }

        super(config);
        RealmSingleton.#instance = this;
    }

    close() {
        RealmSingleton.#instance = null;
        super.close();
    }
}

const {
    RealmProvider,
    useObject: useSyncObject,
    useQuery: useSyncQuery,
    useRealm: useSyncRealm,
} = createRealmContext();

// Has to be a child of UserProvider
const SyncRealmProvider = ({ children }) => {
    const user = useUser();

    const realmSingleton = useMemo(
        () =>
            new RealmSingleton({
                ...syncRealmConfig,
                sync: { ...syncRealmConfig.sync, user },
            }),
        [user]
    );

    return <RealmProvider realm={realmSingleton}>{children}</RealmProvider>;
};

// In App.js
const App = () => {
    return(
        <AppProvider id={...}>
            <UserProvider fallback={Login}>
                <SyncRealmProvider>
                     ...
                </SyncRealmProvider>
            </UserProvider>
        </AppProvider>
    )
}
  • I kept the singleton idea but converted it to a class but it works exactly the same.
  • I used createRealmContext to also have hooks

The only remaining problem is that I can’t set a fallback prop on RealmProvider now as I’m providing a realm prop. I’ll have to find a way to display a fallback manually while the realm is being opened.

Anyway thanks again for the very helpful answers

Cheers,

Renaud

1 Like

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.