Skip to content

Commit

Permalink
fixup: add context sensitivity
Browse files Browse the repository at this point in the history
Signed-off-by: Todd Baert <[email protected]>
  • Loading branch information
toddbaert committed Jan 11, 2024
1 parent af77805 commit b7a4236
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 20 deletions.
122 changes: 112 additions & 10 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,34 @@

🧪 This SDK is experimental.

## Basic Usage

Here's a basic example of how to use the current API with flagd:
Here's a basic example of how to use the current API with the in-memory provider:

```js
```tsx
import logo from './logo.svg';
import './App.css';
import { OpenFeatureProvider, useFeatureFlag, OpenFeature } from '@openfeature/react-sdk';
import { FlagdWebProvider } from '@openfeature/flagd-web-provider';

const provider = new FlagdWebProvider({
host: 'localhost',
port: 8013,
tls: false,
maxRetries: 0,
});
OpenFeature.setProvider(provider)
const flagConfig = {
'new-message': {
disabled: false,
variants: {
on: true,
off: false,
},
defaultVariant: "on",
contextEvaluator: (context: EvaluationContext) => {
if (context.silly) {
return 'on';
}
return 'off'
}
},
};

OpenFeature.setProvider(new InMemoryProvider(flagConfig));

function App() {
return (
Expand All @@ -52,7 +64,7 @@ function App() {
}

function Page() {
const booleanFlag = useFeatureFlag('new-welcome-message', false);
const booleanFlag = useFeatureFlag('new-message', false);
return (
<div className="App">
<header className="App-header">
Expand All @@ -65,3 +77,93 @@ function Page() {

export default App;
```

### Multiple Providers and Scoping

Multiple providers and scoped clients can be configured by passing a `clientName` to the `OpenFeatureProvider`:

```tsx
// Flags within this scope will use the a client/provider associated with `myClient`,
function App() {
return (
<OpenFeatureProvider clientName={'myClient'}>
<Page></Page>
</OpenFeatureProvider>
);
}
```

This is analogous to:

```ts
OpenFeature.getClient('myClient');
```

### Re-rendering with Context Changes

By default, if the OpenFeature [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) is modified, components will be re-rendered.
This is useful in cases where flag values are dependant on user-attributes or other application state (user logged in, items in card, etc).
You can disable this feature in the `useFeatureFlag` hook options:

```tsx
function Page() {
const booleanFlag = useFeatureFlag('new-message', false, { updateOnContextChanged: false });
return (
<MyComponents></MyComponents>
)
}
```

### Re-rendering with Flag Configuration Changes

By default, if the underlying provider emits a `ConfigurationChanged` event, components will be re-rendered.
This is useful if you want your UI to immediately reflect changes in the backend flag configuration.
You can disable this feature in the `useFeatureFlag` hook options:

```tsx
function Page() {
const booleanFlag = useFeatureFlag('new-message', false, { updateOnConfigurationChanged: false });
return (
<MyComponents></MyComponents>
)
}
```

Note that if your provider doesn't support realtime updates, this configuration has no impact.

### Suspense Support

Frequently, providers need to perform some initial startup tasks.
It may be desireable not to display components with feature flags until this is complete.
Built-in [suspense](https://react.dev/reference/react/Suspense) support makes this easy:

```tsx
function Content() {
// cause the "fallback" to be displayed if the component uses feature flags and the provider is not ready
return (
<Suspense fallback={<Fallback />}>
<Message />
</Suspense>
);
}

function Message() {
// component to render after READY.
const { value: showNewMessage } = useFeatureFlag('new-message', false);

return (
<>
{showNewMessage ? (
<p>Welcome to this OpenFeature-enabled React app!</p>
) : (
<p>Welcome to this plain old React app!</p>
)}
</>
);
}

function Fallback() {
// component to render before READY.
return <p>Waiting for provider to be ready...</p>;
}
```
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"homepage": "https://github.com/open-feature/js-sdk#readme",
"peerDependencies": {
"@openfeature/web-sdk": ">=0.4.0",
"@openfeature/web-sdk": ">=0.4.10",
"react": ">=18.0.0"
},
"devDependencies": {
Expand Down
36 changes: 27 additions & 9 deletions packages/react/src/use-feature-flag.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Client, EvaluationDetails, FlagEvaluationOptions, FlagValue, ProviderEvents, ProviderStatus } from '@openfeature/web-sdk';
import { Client, EvaluationDetails, FlagEvaluationOptions, FlagValue, OpenFeature, ProviderEvents, ProviderStatus } from '@openfeature/web-sdk';

Check failure on line 1 in packages/react/src/use-feature-flag.ts

View workflow job for this annotation

GitHub Actions / build-test-lint (16.x)

'OpenFeature' is defined but never used

Check failure on line 1 in packages/react/src/use-feature-flag.ts

View workflow job for this annotation

GitHub Actions / build-test-lint (18.x)

'OpenFeature' is defined but never used

Check failure on line 1 in packages/react/src/use-feature-flag.ts

View workflow job for this annotation

GitHub Actions / build-test-lint (20.x)

'OpenFeature' is defined but never used
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useOpenFeatureClient } from './provider';

Expand All @@ -10,16 +10,25 @@ type ReactFlagEvaluationOptions = {
*/
suspend?: boolean,
/**
* Update the component if the the provider emits a change event.
* Set to false if you never want to update components live, even if flag value changes
* Update the component if the provider emits a ConfigurationChanged event.
* Set to false to prevent components from re-rendering when flag value changes
* are received by the associated provider.
* Defaults to true.
*/
updateOnChange?: boolean,
updateOnConfigurationChanged?: boolean,
/**
* Update the component when the OpenFeature context changes.
* Set to false to prevent components from re-rendering when attributes which
* may be factors in flag evaluation change.
* Defaults to true.
*/
updateOnContextChanged?: boolean,
} & FlagEvaluationOptions;

const DEFAULT_OPTIONS: ReactFlagEvaluationOptions = {
suspend: true
updateOnContextChanged: true,
updateOnConfigurationChanged: true,
suspend: true,
};

enum SuspendState {
Expand All @@ -39,25 +48,34 @@ enum SuspendState {
export function useFeatureFlag<T extends FlagValue>(flagKey: string, defaultValue: T, options?: ReactFlagEvaluationOptions): EvaluationDetails<T> {
const defaultedOptions = { ...DEFAULT_OPTIONS, ...options };
const [, updateState] = useState<object | undefined>();
const forceUpdate = () => updateState({});
const forceUpdate = () => {
updateState({});
};
const client = useOpenFeatureClient();

useEffect(() => {

if (client.providerStatus !== ProviderStatus.READY) {
client.addHandler(ProviderEvents.Ready, forceUpdate); // update the UI when the provider is ready
// update when the provider is ready
client.addHandler(ProviderEvents.Ready, forceUpdate);
if (defaultedOptions.suspend) {
suspend(client, updateState);
}
}

if (defaultedOptions.updateOnChange) {
if (defaultedOptions.updateOnContextChanged) {
// update when the context changes
client.addHandler(ProviderEvents.ContextChanged, forceUpdate);
}

if (defaultedOptions.updateOnConfigurationChanged) {
// update when the provider configuration changes
client.addHandler(ProviderEvents.ConfigurationChanged, forceUpdate);
}
return () => {
// cleanup the handlers
// cleanup the handlers (we can do this unconditionally with no impact)
client.removeHandler(ProviderEvents.Ready, forceUpdate);
client.removeHandler(ProviderEvents.ContextChanged, forceUpdate);
client.removeHandler(ProviderEvents.ConfigurationChanged, forceUpdate);
};
}, [client]);
Expand Down

0 comments on commit b7a4236

Please sign in to comment.