Skip to content

Commit

Permalink
feat: [FFM-10522]: Add useFeatureFlagsClient hook and withFeatureFlag…
Browse files Browse the repository at this point in the history
…sClient HOC (#14)
  • Loading branch information
knagurski authored Feb 5, 2024
1 parent bdaf152 commit 3f9ffee
Show file tree
Hide file tree
Showing 14 changed files with 174 additions and 11 deletions.
63 changes: 62 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ const myFlagValues = useFeatureFlags({

### `useFeatureFlagsLoading`

The `useFeatureFlagsLoading` hook returns a boolean value indicating whether or not the SDK is currently loading Flags
The `useFeatureFlagsLoading` hook returns a boolean value indicating whether the SDK is currently loading Flags
from the server.

```typescript jsx
Expand Down Expand Up @@ -283,6 +283,38 @@ function MyComponent() {
}
```

### `useFeatureFlagsClient`

The React Client SDK internally uses the Javascript Client SDK to communicate with Harness. Sometimes it might be useful
to be able to access the instance of the Javascript Client SDK rather than use the existing hooks or higher-order
components (HOCs). The `useFeatureFlagsClient` hook returns the current Javascript Client SDK instance that the React
Client SDK is using. This instance will be configured, initialized and have been hooked up to the various events the
Javascript Client SDK provides.

```typescript jsx
import {
useFeatureFlagsClient,
useFeatureFlagsLoading
} from '@harnessio/ff-react-client-sdk'

// ...

function MyComponent() {
const client = useFeatureFlagsClient()
const loading = useFeatureFlagsLoading()

if (loading || !client) {
return <p>Loading...</p>
}

return (
<p>
My flag value is: {client.variation('flagIdentifier', 'default value')}
</p>
)
}
```

### `ifFeatureFlag`

The `ifFeatureFlag` higher-order component (HOC) wraps your component and conditionally renders only when the named flag
Expand Down Expand Up @@ -380,6 +412,35 @@ function MyComponent({ flags, loading }) {
const MyComponentWithFlags = withFeatureFlags(MyComponent)
```

### `withFeatureFlagsClient`

The React Client SDK internally uses the Javascript Client SDK to communicate with Harness. Sometimes it might be useful
to be able to access the instance of the Javascript Client SDK rather than use the existing hooks or higher-order
components (HOCs). The `withFeatureFlagsClient` HOC wraps your component and adds `featureFlagsClient` as additional
prop. `featureFlagsClient` is the current Javascript Client SDK instance that the React Client SDK is using. This
instance will be configured, initialized and have been hooked up to the various events the Javascript Client SDK
provides.

```typescript jsx
import { withFeatureFlagsClient } from '@harnessio/ff-react-client-sdk'

// ...

function MyComponent({ featureFlagsClient }) {
if (featureFlagsClient) {
return (
<p>
Flag1's value is {featureFlagsClient.variation('flag1', 'no value')}
</p>
)
}

return <p>The Feature Flags client is not currently available</p>
}

const MyComponentWithClient = withFeatureFlagsClient(MyComponent)
```

## Testing with Jest

When running tests with Jest, you may want to mock the SDK to avoid making network requests. You can do this by using
Expand Down
2 changes: 1 addition & 1 deletion examples/get-started/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@harnessio/ff-react-client-sdk",
"version": "1.7.0",
"version": "1.8.0",
"author": "Harness",
"license": "Apache-2.0",
"module": "dist/esm/index.js",
Expand Down
2 changes: 1 addition & 1 deletion src/hoc/__tests__/ifFeatureFlag.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { ComponentType } from 'react'
import { ifFeatureFlag } from '../ifFeatureFlag'
import { render, RenderResult, screen } from '@testing-library/react'
import { FFContextValue } from '../../context/FFContext'
import { TestWrapper } from '../../test-utils/TestWrapper'
import { TestWrapper } from '../../test-utils'

const SampleComponent = () => (
<span data-testid="sample-component">Sample Component</span>
Expand Down
2 changes: 1 addition & 1 deletion src/hoc/__tests__/withFeatureFlags.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { ComponentType, FC } from 'react'
import { render, RenderResult, screen } from '@testing-library/react'
import { FFContextValue } from '../../context/FFContext'
import { withFeatureFlags } from '../withFeatureFlags'
import { TestWrapper } from '../../test-utils/TestWrapper'
import { TestWrapper } from '../../test-utils'

const SampleComponent: FC = (props) => (
<span data-testid="sample-component">{JSON.stringify(props)}</span>
Expand Down
44 changes: 44 additions & 0 deletions src/hoc/__tests__/withFeatureFlagsClient.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { ComponentType, FC } from 'react'
import { render, RenderResult, screen } from '@testing-library/react'
import { TestWrapper } from '../../test-utils'
import { withFeatureFlagsClient } from '../withFeatureFlagsClient'

const SampleComponent: FC = (props) => (
<span data-testid="sample-component">{JSON.stringify(props)}</span>
)

const renderComponent = (WrappedComponent: ComponentType): RenderResult =>
render(<WrappedComponent />, {
wrapper: ({ children }) => (
<TestWrapper flags={{ f1: 'f1' }}>{children}</TestWrapper>
)
})

describe('withFeatureFlagsClient', () => {
test('it should add the featureFlagsClient prop to the component', async () => {
const WrappedComponent = withFeatureFlagsClient(SampleComponent)

renderComponent(WrappedComponent)

const el = screen.getByTestId('sample-component')
expect(el).toBeInTheDocument()
expect(el).toHaveTextContent('featureFlagsClient')
})

test('it should maintain existing props', async () => {
const extraProps = { hello: 'world', abc: 123 }
const WrappedComponent =
withFeatureFlagsClient<Record<string, any>>(SampleComponent)

const WrappedComponentWithExtraProps = () => (
<WrappedComponent {...extraProps} />
)

renderComponent(WrappedComponentWithExtraProps)
const el = screen.getByTestId('sample-component')
expect(el).toBeInTheDocument()
expect(el).toHaveTextContent(
JSON.stringify({ featureFlagsClient: {}, ...extraProps })
)
})
})
4 changes: 3 additions & 1 deletion src/hoc/withFeatureFlags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { useFeatureFlags } from '../hooks/useFeatureFlags'
import { useFeatureFlagsLoading } from '../hooks/useFeatureFlagsLoading'

export function withFeatureFlags<C>(
WrappedComponent: ComponentType<C & FFContextValue>
WrappedComponent: ComponentType<
C & { flags: FFContextValue['flags']; loading: FFContextValue['loading'] }
>
) {
return (props: C) => (
<WithFeatureFlagsComponent
Expand Down
28 changes: 28 additions & 0 deletions src/hoc/withFeatureFlagsClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { ComponentType } from 'react'
import { FFContextValue } from '../context/FFContext'
import { useFeatureFlagsClient } from '../hooks/useFeatureFlagsClient'

export function withFeatureFlagsClient<C>(
WrappedComponent: ComponentType<
C & { featureFlagsClient: FFContextValue['client'] }
>
) {
return (props: C) => (
<WithFeatureFlagsClientComponent
WrappedComponent={WrappedComponent}
componentProps={props}
/>
)
}

function WithFeatureFlagsClientComponent({
WrappedComponent,
componentProps
}: {
WrappedComponent: ComponentType<any>
componentProps: any
}) {
const client = useFeatureFlagsClient()

return <WrappedComponent featureFlagsClient={client} {...componentProps} />
}
2 changes: 1 addition & 1 deletion src/hooks/__tests__/useFeatureFlag.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { renderHook, RenderHookResult } from '@testing-library/react'
import { useFeatureFlag } from '../useFeatureFlag'
import { TestWrapper } from '../../test-utils/TestWrapper'
import { TestWrapper } from '../../test-utils'

const renderUseFeatureFlagHook = (
args: Parameters<typeof useFeatureFlag>,
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/__tests__/useFeatureFlags.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { renderHook, RenderHookResult } from '@testing-library/react'
import { useFeatureFlags } from '../useFeatureFlags'
import { TestWrapper } from '../../test-utils/TestWrapper'
import { TestWrapper } from '../../test-utils'

const renderUseFeatureFlagsHook = (
args: Parameters<typeof useFeatureFlags>,
Expand Down
20 changes: 20 additions & 0 deletions src/hooks/__tests__/useFeatureFlagsClient.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { renderHook, RenderHookResult } from '@testing-library/react'
import { TestWrapper } from '../../test-utils'
import { useFeatureFlagsClient } from '../useFeatureFlagsClient'
import { FFContextValue } from '../../context/FFContext'

const renderUseFeatureFlagsClientHook = (): RenderHookResult<
FFContextValue['client'],
any
> =>
renderHook(() => useFeatureFlagsClient(), {
wrapper: ({ children }) => <TestWrapper flags={{}}>{children}</TestWrapper>
})

describe('useFeatureFlagsClient', () => {
test('it should return the client', async () => {
const { result } = renderUseFeatureFlagsClientHook()

expect(result.current).toHaveProperty('variation')
})
})
2 changes: 1 addition & 1 deletion src/hooks/__tests__/useFeatureFlagsLoading.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { renderHook, RenderHookResult } from '@testing-library/react'
import { TestWrapper } from '../../test-utils/TestWrapper'
import { TestWrapper } from '../../test-utils'
import { useFeatureFlagsLoading } from '../useFeatureFlagsLoading'

const renderUseFeatureFlagsLoadingHook = (
Expand Down
8 changes: 8 additions & 0 deletions src/hooks/useFeatureFlagsClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FFContext, FFContextValue } from '../context/FFContext'
import { useContext } from 'react'

export const useFeatureFlagsClient = (): FFContextValue['client'] => {
const { client } = useContext(FFContext)

return client
}

0 comments on commit 3f9ffee

Please sign in to comment.