@flagward/vue
v0.3.0
Published
Vue SDK for Flagward - Feature Flags as a Service
Maintainers
Readme
@flagward/vue
Vue SDK for Flagward. Flags are downloaded once and evaluated in your process, so a flag check costs nothing and never blocks on the network.
Installation
npm install @flagward/vueRequires Vue 3.5 or newer. @flagward/core comes with it — you do not install
it separately.
Quick start
Install the plugin once, on the app:
// main.ts
import { createApp } from "vue";
import { flagward } from "@flagward/vue";
import App from "./App.vue";
createApp(App)
.use(flagward({
apiKey: "your-environment-api-key",
host: "https://flags.example.com",
}))
.mount("#app");Then ask for a flag wherever the decision is actually made:
<script setup lang="ts">
import { useFlag } from "@flagward/vue";
const { value, isLoading } = useFlag("new-checkout");
</script>
<template>
<LegacyCheckout v-if="isLoading" />
<NewCheckout v-else-if="value" />
<LegacyCheckout v-else />
</template>host is optional and defaults to the hosted service at
https://app.flagward.com. A self-hosted install passes its own — and that
is the case that has to be configured either way, since its operator already
knows they are running something.
Nothing has to be wrapped. That is the point of installing on the app rather than providing from a component: a flag is asked for where the decision is made, not where somebody remembered to put a provider.
The value is a ref
Composables return ComputedRefs, so destructuring keeps reactivity. Templates
unwrap them for you; in <script> you read .value:
const { value } = useFlag("new-checkout");
if (value.value) { /* ... */ }value is undefined while loading, and stays undefined for a key this
environment does not have — so decide what an unknown flag means rather than
letting undefined decide for you.
How flags are evaluated
A flag with no rules is its own on/off switch. A flag with rules is evaluated against a context you supply: rules are tried in priority order and the first match wins.
Set the context once, for the whole app:
app.use(flagward({ apiKey, context: { plan: "pro", country: "AR" } }));Or add to it for one call:
const { value } = useFlag("beta", { betaTester: true });The context can be reactive
Everywhere a context is taken — the plugin, useFlag, getFlag — it accepts a
ref or a getter as readily as a plain object:
const user = ref({ plan: "standard" });
const { value } = useFlag("beta", user);
user.value = { plan: "pro" }; // the flag re-evaluates, the DOM updates// Or as a getter, when the attributes come from several sources:
const { value } = useFlag("beta", () => ({ plan: plan.value, country: geo.country }));This matters because what an application targets on is not fixed: somebody signs in, changes plan, switches locale. A context read once at startup would answer every later evaluation with what was true at startup.
Supported operators: EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN,
IN_LIST, CONTAINS, combined with AND or OR.
Composables
Which one
Reach for useFlag. One flag, one decision, one composable — it is what most
components need.
useFlags earns its place in three cases:
- The keys are not known where you write the code — a debug panel, an admin view, anything that iterates.
- You need a flag where a composable cannot go — inside an event handler, a
callback, a conditional branch. Composables run in
setup;getFlagruns whenever you call it. - A component reads several flags and one call reads better than five.
useFlag(key, context?)
const { value, isLoading, error } = useFlag("new-checkout");| | |
| --- | --- |
| value | ComputedRef<boolean \| undefined> |
| isLoading | ComputedRef<boolean> — true until the first snapshot arrives |
| error | ComputedRef<Error \| null> — the last startup failure, cleared when flags arrive |
useFlags()
For a component that reads several flags, or iterates them:
const { flags, isLoading, error, getFlag } = useFlags();
flags.value; // { "new-checkout": true, ... }
getFlag("beta"); // one flag, app context
getFlag("beta", { plan: "pro" }); // one flag, this contextuseVariant(key, context?)
Resolves the variant of a MULTIVARIATE flag.
const { value, isLoading, error } = useVariant("checkout-flow");| | |
| --- | --- |
| value | ComputedRef<string \| undefined> — the variant's name, undefined while loading, for a key this environment does not have, and for a flag that is not MULTIVARIATE, is disabled, or is overridden (an override forces a boolean value server-side, so there is no variant) |
| isLoading | ComputedRef<boolean> — true until the first snapshot arrives |
| error | ComputedRef<Error \| null> — the last startup failure, cleared when flags arrive |
useFlag and useFlags keep returning true once a MULTIVARIATE flag is
enabled — they answer "is it on", not "which variant". Reach for useVariant
when the answer needs to be the variant itself.
Where context comes from
Targeting rules are evaluated against a context, and there are two places it can come from. They are not interchangeable:
app.use(flagward({ apiKey, context: user })); // who the user is
useFlag("beta", { plan: "pro" }); // just this callA context passed to useFlag belongs to that call. It is not published
anywhere: another component cannot see it, and the map useFlags returns
resolves against the plugin's context alone. So this is not a contradiction —
const { value } = useFlag("beta", { plan: "pro" }); // true
const { flags } = useFlags(); // flags.value.beta === false— it is two questions with two answers. The call was told "pro"; the map was
not. Deliberately: if a context passed in one component reached another
component's composable, you would have an invisible channel between parts of an
application that share nothing.
Putting it together
The user lives in the plugin, once:
// main.ts
import { createApp, ref } from "vue";
import { flagward } from "@flagward/vue";
const user = ref({ plan: "free", country: "AR", id: null as string | null });
createApp(App)
.use(flagward({
apiKey: import.meta.env.VITE_FLAGWARD_API_KEY,
host: "https://flags.example.com",
context: user,
}))
.mount("#app");
// Signing in updates the ref, and every flag in the application follows.
export function signIn(account: Account) {
user.value = { plan: account.plan, country: account.country, id: account.id };
}One decision, one flag:
<!-- Checkout.vue -->
<script setup lang="ts">
import { useFlag } from "@flagward/vue";
const { value: newCheckout, isLoading } = useFlag("new-checkout");
</script>
<template>
<LegacyCheckout v-if="isLoading" />
<NewCheckout v-else-if="newCheckout" />
<LegacyCheckout v-else />
</template>A flag inside a handler, where a composable cannot go:
<!-- CheckoutForm.vue -->
<script setup lang="ts">
import { useFlags } from "@flagward/vue";
const { getFlag } = useFlags();
function onSubmit(data: FormData) {
if (getFlag("strict-validation") && !isComplete(data)) {
return setError("Every field is required.");
}
submit(data);
}
</script>And the one case for a per-call context — the flag is about each row, not about whoever is looking:
<!-- UserTable.vue -->
<script setup lang="ts">
import { useFlags } from "@flagward/vue";
defineProps<{ users: User[] }>();
const { getFlag } = useFlags();
</script>
<template>
<tr v-for="u in users" :key="u.id">
<td>{{ u.name }}</td>
<td>{{ getFlag("premium-badge", { plan: u.plan }) ? "★" : "" }}</td>
</tr>
</template>Multivariate flags
A MULTIVARIATE flag does not just turn a feature on — it hands back which
variant a user is in. useFlag and useFlags keep answering "is it on"
(true once the flag is enabled, even for a MULTIVARIATE flag); reach for
useVariant above when the answer needs to be the
variant name.
Put user_id in the context
Variant assignment is a deterministic bucket of user_id and the flag's
key — the same user always lands in the same variant. Without a user_id,
every user resolves to the flag's control variant. Set it alongside
whatever else your rules target:
app.use(flagward({ apiKey, context: { user_id: user.id, plan: user.plan } }));A/B/n experiment
<script setup lang="ts">
import { useVariant } from "@flagward/vue";
const { value: variant, isLoading } = useVariant("checkout-flow");
</script>
<template>
<LegacyCheckout v-if="isLoading" />
<OnePageCheckout v-else-if="variant === 'one-page'" />
<ExpressCheckout v-else-if="variant === 'express'" />
<LegacyCheckout v-else /> <!-- control, or undefined -->
</template>Variant as remote configuration
A variant name is also a lookup key, not just a branch to render on. In
<script>, read through .value; a computed keeps the lookup reactive:
import { computed } from "vue";
import { useVariant } from "@flagward/vue";
const COPY: Record<string, string> = {
control: "Buy now",
urgent: "Buy now — 3 left",
};
const { value: variant } = useVariant("cta-copy");
const label = computed(() => COPY[variant.value ?? ""] ?? "Buy now");Segment targeting
Per-call context is reactive the same way it is for useFlag — a ref or a
getter — and a rule can send part of a segment to a specific variant while the
rest falls through to the flag's overall split:
const plan = ref(user.plan);
const { value: variant } = useVariant("pricing-page", () => ({ plan: plan.value }));
// e.g. a rule sends 20% of plan.value === "enterprise" to "annual-discount";
// the other 80% falls through to the flag's own split.Loading
const { value, isLoading } = useVariant("checkout-flow");
if (isLoading.value) { /* show a spinner */ }
else if (value.value === "express") { /* ... */ }Losing the network
The plugin opens a server-sent events stream and keeps it in step on its own:
- A flag changed on the server reaches every component within a second.
- Coming back online re-reads the flags, because a dropped connection means missed events — reconnecting alone would serve a stale value indefinitely.
- Returning to a backgrounded tab does the same. A machine waking from sleep drops the connection without the browser ever reporting the network as gone.
- Where there is no
EventSource— server rendering, plain Node — live updates are reported as off and the flags already read keep working.
A failure never propagates into your application. Flags keep their last known values and each caller's own fallback decides what the user sees.
Error reporting
Everything the SDK recovers from is reported to the console once per page load,
prefixed with [Flagward]: a missing or rejected API key, an unreachable host,
a flag that is not in this environment, a dropped stream. Narrow it with
logLevel:
app.use(flagward({ apiKey, logLevel: "error" })); // "warn" | "error" | "silent"Outside Vue
The client and the evaluator are re-exported here, so a store, a router guard or a plain module can use them without a component:
import { FlagwardClient, evaluateFlag } from "@flagward/vue";Module format
ESM only, with type declarations. There is no CommonJS build.
License
MIT
