---
title: Implement a user profile service for the React SDK
slug: feature-experimentation/docs/implement-a-user-profile-service-for-the-react-sdk
docTags: 
createdAt: 2026-08-05T20:40:03.000Z
---

Use a **User Profile Service** to persist information about your users and ensure variation assignments are sticky. Sticky implies that when a user gets a particular variation, their assignment will not change.

In the React SDK, there is no default implementation. Implementing a User Profile Service is optional and is only necessary if you want to keep variation assignments sticky even when experiment conditions are changed while it is running (for example, audiences, attributes, variation pausing, and traffic distribution). Otherwise, the React SDK is stateless and relies on deterministic bucketing to return consistent assignments.

If the User Profile Service does not bucket a user as you expect, then check whether other flags are overriding the bucketing. For information, see [How bucketing works](docId\:eH-UMm_rOn_GdGDGLV6__).

## Implement a service (sync)

To implement a custom User Profile Service, the service needs to expose two functions with the following signatures:

- `lookup` – Takes a user ID string and returns a user profile matching the schema below.
- `save` – Takes a user profile and persists it.

If you want to use the User Profile Service purely for tracking purposes and not sticky bucketing, you can implement only the `save` method (always return `null` from `lookup`).

Pass the synchronous service to `createInstance` using the `userProfileService` option:

```javascript
import {
  createInstance,
  createPollingProjectConfigManager,
  createBatchEventProcessor,
} from '@optimizely/react-sdk';

const userProfileService = {
  lookup: (userId) => {
    // Perform user profile lookup (e.g., from localStorage)
  },
  save: (userProfileMap) => {
    // Persist user profile (e.g., to localStorage)
  },
};

const optimizely = createInstance({
  projectConfigManager: createPollingProjectConfigManager({
    sdkKey: 'YOUR_SDK_KEY',
  }),
  eventProcessor: createBatchEventProcessor(),
  userProfileService,
});
```

## Implement a service (async)

The React SDK v4 also supports an asynchronous User Profile Service for use with backends like Redis, Cassandra, or any storage that requires async I/O.

:::hint{type="warning"}
**Important**
If you use an asynchronous User Profile Service (`UserProfileServiceAsync`), you must use the async decide hooks (`useDecideAsync`, `useDecideForKeysAsync`, `useDecideAllAsync`). The standard synchronous decide hooks do not call the async User Profile Service.
:::

The async service implements the following interface:

```typescript
export interface UserProfileServiceAsync {
  lookup(userId: string): Promise<UserProfile>;
  save(profile: UserProfile): Promise<void>;
}
```

Pass the async service to `createInstance` using the `userProfileServiceAsync` option:

```javascript
import {
  createInstance,
  createPollingProjectConfigManager,
  createBatchEventProcessor,
} from '@optimizely/react-sdk';

const userProfileServiceAsync = {
  lookup: async (userId) => {
    // Fetch user profile from your backend (e.g., Redis, database)
    const response = await fetch(`/api/user-profiles/${userId}`);
    if (!response.ok) return { user_id: userId, experiment_bucket_map: {} };
    return response.json();
  },
  save: async (userProfileMap) => {
    // Persist user profile to your backend
    await fetch(`/api/user-profiles/${userProfileMap.user_id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(userProfileMap),
    });
  },
};

const optimizely = createInstance({
  projectConfigManager: createPollingProjectConfigManager({
    sdkKey: 'YOUR_SDK_KEY',
  }),
  eventProcessor: createBatchEventProcessor(),
  userProfileServiceAsync,
});
```

Then use the async decide hooks in your components:

```javascript
import { useDecideAsync } from '@optimizely/react-sdk';

function MyComponent() {
  const { decision, isLoading, error } = useDecideAsync('my-flag');

  if (isLoading) return <Loading />;
  if (error) return <ErrorDisplay error={error} />;

  return decision.enabled ? <NewFeature /> : <Default />;
}
```

For a more detailed async UPS example using Redis, see [Implement a user profile service for the JavaScript SDK](docId\:SBsPoqyJBy1uGzOZK8Evz).

## User profile schema

The following code example shows the JSON schema for the user profile object.

Use `experiment_bucket_map` to override the default bucketing behavior and define an alternate experiment variation for a given user. For each experiment that you want to override, add an object to the map. Use the experiment ID as the key and include a `variation_id` property that specifies the desired variation. If there isn't an entry for an experiment, then the default bucketing behavior persists.

In the following example, `^[a-zA-Z0-9]+$` is the experiment ID.

```json
{
  "title": "UserProfile",
  "type": "object",
  "properties": {
    "user_id": {"type": "string"},
    "experiment_bucket_map": {"type": "object",
                              "patternProperties": {
                                 "^[a-zA-Z0-9]+$": {"type": "object",
                                                    "properties": {"variation_id": {"type":"string"}},
                                                    "required": ["variation_id"]}
                               }
                             }
  },
  "required": ["user_id", "experiment_bucket_map"]
}
```

The React SDK uses the User Profile Service you provide to override the default bucketing behavior in cases when an experiment assignment has been saved.

When implementing your own User Profile Service, you should load the user profiles into the User Profile Service on initialization and avoid performing expensive, blocking lookups on the lookup function to minimize the performance impact of incorporating the service.
