@avsbhq/solid
v1.0.0
Published
Solid JS SDK for the [A vs B](https://app.avsb.cloud) platform.
Readme
@avsbhq/solid
Solid JS SDK for the A vs B platform.
Drop an <AvsbProvider> at the root of your Solid app and read feature flags from anywhere using reactive createFlag accessors. Signals update automatically when flag values change — no polling, no manual subscriptions, no re-renders outside of the components that read each flag.
1. Install
npm install @avsbhq/solid solid-js@avsbhq/browser ships as a runtime dependency — you do not need to install it separately.
2. Quickstart
// app.tsx
import { AvsbProvider } from '@avsbhq/solid'
export function App() {
return (
<AvsbProvider
sdkKey={import.meta.env.VITE_AVSB_SDK_KEY}
attributes={{ userId: 'user_123', plan: 'pro' }}
>
<YourApp />
</AvsbProvider>
)
}// CheckoutButton.tsx
import { createFlag, useTrack } from '@avsbhq/solid'
export function CheckoutButton() {
const newCheckout = createFlag('new-checkout-flow', false)
const track = useTrack()
return (
<button onClick={() => track('checkout_clicked')}>
{newCheckout().value ? 'Start checkout (new)' : 'Buy now'}
</button>
)
}3. SDK keys
Client SDK keys are safe to ship in browser bundles. Store them in your framework's public env variable:
VITE_AVSB_SDK_KEY=sdk-client-xxxxxxxxGet a client SDK key from your A vs B project's Environments page.
Server-side evaluation (Solid Start middleware) requires a server SDK key. Server keys must never be sent to the client.
4. Identity — identify, updateAttributes, alias, reset
import { useIdentify, useAlias } from '@avsbhq/solid'
function LoginButton() {
const identify = useIdentify()
const alias = useAlias()
const handleLogin = async (user: { id: string; plan: string }) => {
// Stitch anonymous visitor to identified user
await alias({ kind: 'user', key: 'anon' }, { kind: 'user', key: user.id })
// Switch evaluation context to the logged-in user
identify({ kind: 'user', key: user.id, plan: user.plan })
}
return <button onClick={() => handleLogin({ id: 'u_789', plan: 'enterprise' })}>Log in</button>
}The useAvsbClient() accessor returns the underlying client for lower-level calls like client.updateAttributes(...) and client.reset().
5. Multi-context
Pass a MultiContext to identify() or the provider's context option when you need to target flags based on more than one entity (user + organization, user + device):
import type { MultiContext } from '@avsbhq/solid'
const ctx: MultiContext = {
kind: 'multi',
user: { kind: 'user', key: 'u_123', plan: 'pro' },
organization: { kind: 'organization', key: 'org_456', tier: 'enterprise' },
}
identify(ctx)Flag rules in the A vs B platform can target any context kind declared in the project settings.
6. Reading flags
createFlag<T>(key, defaultValue): Accessor<Flag<T>>
Returns a Solid Accessor<Flag<T>>. Read the signal inside reactive computations or JSX. The Flag<T> object includes the typed value plus evaluation metadata.
function PricingPage() {
const pricingExperiment = createFlag('pricing-experiment', 'default')
return (
<Switch>
<Match when={pricingExperiment().variationKey === 'three-tier'}>
<ThreeTierPricing />
</Match>
<Match when={pricingExperiment().variationKey === 'usage-based'}>
<UsageBasedPricing />
</Match>
<Match when={true}>
<DefaultPricing />
</Match>
</Switch>
)
}createFlagValue<T>(key, defaultValue): Accessor<T>
Returns only the flag's value — no metadata. Use when you just need the value:
const showBanner = createFlagValue('holiday-banner', false)
return <Show when={showBanner()}><HolidayBanner /></Show>Typed variants
| Accessor | Return type |
|---|---|
| createBoolFlag(key, default) | Accessor<Flag<boolean>> |
| createStringFlag(key, default) | Accessor<Flag<string>> |
| createNumberFlag(key, default) | Accessor<Flag<number>> |
| createJsonFlag<T>(key, default) | Accessor<Flag<T>> |
Typed variants call the matching typed SDK method (getBoolFlag, etc.) which validates the flag type at runtime and warns on mismatch.
createAllFlags(): Accessor<Record<string, Flag>>
Returns all flags as a reactive record. Updates on any flag change. Use for debug panels:
function FlagDebugPanel() {
const allFlags = createAllFlags()
return (
<For each={Object.entries(allFlags())}>
{([key, flag]) => <div>{key}: {String(flag.value)}</div>}
</For>
)
}Dynamic flag keys
All create* accessors accept a reactive Accessor<string> as the key argument:
const [activeExperiment, setActiveExperiment] = createSignal('exp-a')
const flag = createFlag(activeExperiment, false)
// Automatically re-subscribes when activeExperiment() changes.7. Tracking events
import { useTrack } from '@avsbhq/solid'
function SignupForm() {
const track = useTrack()
const onSubmit = async (data: { plan: string }) => {
await api.signup(data)
track('signup_completed', { properties: { plan: data.plan } })
}
return <Form onSubmit={onSubmit} />
}TrackPayload shape:
interface TrackPayload {
value?: number // numeric metric (e.g. order value)
properties?: Record<string, unknown>
context?: EvalContext // override context (server SDK only)
}8. Error handling
The provider exposes error state. Use createFlagReady() to gate rendering until the SDK is ready, or read the context error directly:
import { createFlagReady, useAvsbContext } from '@avsbhq/solid'
function GatedContent() {
const isReady = createFlagReady()
const { status, error } = useAvsbContext()
return (
<Switch>
<Match when={status() === 'loading'}>Loading flags...</Match>
<Match when={status() === 'error'}>
<p>Feature flags unavailable: {error()?.message}</p>
</Match>
<Match when={isReady()}>
<MainContent />
</Match>
</Switch>
)
}Configure a custom logger in the provider options:
<AvsbProvider
sdkKey={...}
logger={{
info: (msg, data) => myLogger.info(msg, data),
warn: (msg, data) => myLogger.warn(msg, data),
error: (msg, data) => myLogger.error(msg, data),
debug: () => {},
child: (prefix) => ({ ...myLogger, prefix }),
}}
>
...
</AvsbProvider>9. SSR / hydration (Solid Start)
Use the withAvsbServerContext middleware and serializeAvsbBootstrap helper to pre-evaluate flags server-side and hydrate the client provider from the bootstrap payload:
// src/middleware.ts
import { withAvsbServerContext } from '@avsbhq/solid/solid-start'
export default withAvsbServerContext(
(event) => event,
{
sdkKey: process.env.AVSB_SDK_KEY!,
getContext: (event) => ({
kind: 'user',
key: event.request.headers.get('x-user-id') ?? 'anonymous',
}),
}
)// src/routes/index.tsx
import { createRouteData } from 'solid-start'
import { serializeAvsbBootstrap } from '@avsbhq/solid/solid-start'
import { serverSdkClient } from '~/lib/avsb-server'
import { AvsbProvider } from '@avsbhq/solid'
export const routeData = createRouteData(async (_, { request }) => {
const userId = request.headers.get('x-user-id') ?? 'anonymous'
const ctx = { kind: 'user' as const, key: userId }
const bootstrap = await serializeAvsbBootstrap(serverSdkClient, ctx)
return { bootstrap }
})
export default function Page() {
const data = useRouteData<typeof routeData>()
return (
<AvsbProvider sdkKey={import.meta.env.VITE_AVSB_SDK_KEY} bootstrap={data()?.bootstrap}>
<PageContent />
</AvsbProvider>
)
}10. Graceful shutdown
In Mode A (provider creates the client), the client is automatically closed when the <AvsbProvider> unmounts via Solid's onCleanup. This flushes queued events and stops background polling.
In Mode B (caller injects a pre-built client), the caller owns the lifecycle. Close it explicitly:
const client = new AvsbClient({ sdkKey: '...' })
// Later, when cleaning up:
await client.close()11. Testing
Use @avsbhq/test for unit tests. Provide a mock client via Mode B injection to avoid network calls:
import { createMockClient } from '@avsbhq/test'
import { render } from 'solid-testing-library'
import { AvsbProvider } from '@avsbhq/solid'
import { MyComponent } from './MyComponent'
test('shows variant-a when flag is enabled', () => {
const mockClient = createMockClient({
flags: {
'pricing-experiment': { value: 'variant-a', variationKey: 'variant-a', source: 'rule' },
},
})
const { getByText } = render(() => (
<AvsbProvider client={mockClient}>
<MyComponent />
</AvsbProvider>
))
expect(getByText('Three-tier pricing')).toBeInTheDocument()
})For logic-level tests (signal behavior without DOM rendering), use Solid's createRoot directly:
import { createRoot, createSignal } from 'solid-js'
test('flag signal updates on change', () => {
createRoot((dispose) => {
// ... reactive test logic
dispose()
})
})12. Migration from LaunchDarkly
If you are migrating from launchdarkly-js-client-sdk:
| LaunchDarkly | @avsbhq/solid |
|---|---|
| client.variation('key', false) | createFlagValue('key', false)() |
| client.variationDetail('key', false) | createFlag('key', false)() |
| client.allFlags() | createAllFlags()() |
| client.track('event', null, 99) | useTrack()('event', { value: 99 }) |
| client.identify(ctx) | useIdentify()(ctx) |
The A vs B Flag<T> object includes the same information as LDEvaluationDetail (value, variationIndex mapped to variationKey, reason mapped to reasons array).
For a full migration guide including audience rule translations and SDK key changes, see the LaunchDarkly migration guide.
Requirements
- Solid JS >= 1.8 (peer dependency)
- Modern browser supporting ES2020
