Skip to content

API Design Issue: Simplifying Client Instance Management Across Screens #93

@saseungmin

Description

@saseungmin

Context

I'm currently integrating reCAPTCHA Enterprise into a React Native app and noticed that the recommended API (Recaptcha.fetchClient) requires manual management of the RecaptchaClient instance across different screens. I wanted to open a discussion about potential improvements to make this easier for developers.

Current API (Recommended)

import { Recaptcha, RecaptchaAction, RecaptchaClient } from '@google-cloud/recaptcha-enterprise-react-native';

// Must initialize once
const client = await Recaptcha.fetchClient(siteKey);

// Now need to pass this client to other screens...
// How do we share this across the app?

The Challenge

The documentation states:

You must initialize the reCAPTCHA client only once during the lifetime of your app.

However, the API returns a client instance that needs to be shared across multiple screens (e.g., Login, SignUp, Password Reset). This requires developers to implement additional patterns to manage the instance:

Solution 1: Context API (Boilerplate Heavy)

// RecaptchaContext.tsx
const RecaptchaContext = createContext<RecaptchaClient | null>(null);

export const RecaptchaProvider = ({ children }) => {
  const [client, setClient] = useState<RecaptchaClient | null>(null);

  useEffect(() => {
    Recaptcha.fetchClient(siteKey).then(setClient);
  }, []);

  return (
    <RecaptchaContext.Provider value={client}>
      {children}
    </RecaptchaContext.Provider>
  );
};

export const useRecaptcha = () => useContext(RecaptchaContext);

// App.tsx
<RecaptchaProvider>
  <Navigation />
</RecaptchaProvider>

// LoginScreen.tsx
const client = useRecaptcha();
const token = await client?.execute(RecaptchaAction.LOGIN());

Solution 2: Singleton Pattern

// recaptchaService.ts
class RecaptchaService {
  private client: RecaptchaClient | null = null;

  async initialize(siteKey: string) {
    if (!this.client) {
      this.client = await Recaptcha.fetchClient(siteKey);
    }
    return this.client;
  }

  getClient() {
    if (!this.client) throw new Error('Not initialized');
    return this.client;
  }
}

export const recaptchaService = new RecaptchaService();

// Usage
await recaptchaService.initialize(siteKey);
const token = await recaptchaService.getClient().execute(RecaptchaAction.LOGIN());

Both solutions work but require additional setup code.

Observation: The Deprecated API Pattern

I noticed the deprecated API (initClient + execute) had a simpler usage pattern:

import { initClient, execute, RecaptchaAction } from '@google-cloud/recaptcha-enterprise-react-native';

// Initialize once
await initClient(siteKey, 10000);

// Use anywhere in the app - no instance management needed!
const token = await execute(RecaptchaAction.LOGIN(), 10000);

This pattern works well because the native layer already manages the client internally. Looking at the implementation:

Android (RecaptchaEnterpriseReactNativeModule.kt:37):

private lateinit var recaptchaClient: RecaptchaClient  // Stored in module

iOS (RecaptchaEnterpriseReactNative.swift:19):

var recaptchaClient: RecaptchaClient?  // Stored in module

The native modules already handle instance management internally.

Question

What was the reasoning behind deprecating initClient/execute in favor of the instance-based approach?

Reference: Patterns from Similar Libraries

React Native Firebase

import { getAuth, signInWithEmailAndPassword } from '@react-native-firebase/auth';

// Functional API - no manual instance management
await signInWithEmailAndPassword(getAuth(), email, password);

Firebase JS SDK

import { initializeApp } from 'firebase/app';
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';

// Initialize once
const app = initializeApp(config);

// Functional API - pass auth instance to functions
await signInWithEmailAndPassword(getAuth(app), email, password);

AWS Amplify

import { Amplify, Auth } from 'aws-amplify';

// Configure once
Amplify.configure(awsconfig);

// Use anywhere
await Auth.signIn(username, password);

These libraries provide different approaches to instance management that might be worth considering.

Potential Approaches

Approach 1: Static Methods (Similar to Amplify)

// Configure once
await Recaptcha.configure({ siteKey: 'YOUR_SITE_KEY' });

// Use anywhere - no instance needed
const token = await Recaptcha.execute(RecaptchaAction.LOGIN());

// Check if configured
if (Recaptcha.isConfigured()) {
  // Ready to use
}

Approach 2: Improve and Re-introduce Functional API

export { initClient as configure };
export { execute };

// Usage
await configure(siteKey);
const token = await execute(RecaptchaAction.LOGIN());

Potential Benefits

Any of these approaches could provide:

  • Reduced boilerplate for common use cases
  • Easier usage across multiple screens
  • Simpler onboarding experience

I'm happy to contribute an implementation if there's interest in this direction.


Environment:

  • Package version: 18.9.0-beta01
  • React Native version: 0.83.0
  • Platform: iOS & Android

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions