Use cases
Loading datafiles on demand
A large application can have hundreds of features, while any one page needs a few. Instead of one datafile with everything, load a smaller datafile for each part of the application as the user reaches it, and merge them into one SDK instance.
Build a datafile per area#
Tag each feature with the part of the application it belongs to, and give each part a target. Add the area tags to your project configuration, preserving any other settings:
module.exports = { tags: ['all', 'products', 'checkout'],}Tag each feature or global variable with its area, such as tags: [products]. A target with a tag selector includes matching tagged definitions and their dependencies, not every definition by default.
description: Products areatag: productsdescription: Checkout areatag: checkoutWhen you build, each target produces its own datafile, datafiles/featurevisor-products.json and datafiles/featurevisor-checkout.json.
Load each one when it is needed#
setDatafile merges by default: a new datafile's definitions replace those with the same keys, and the rest stay. So one instance can take each area's datafile as the user reaches it.
Publish each build's datafiles under a URL of their own that never changes, such as a release path from your deployment system. The example uses build-124 until you refresh to another release. Only combine datafiles from the same build, so shared definitions agree. Matching revisions alone do not prove that.
Load each target once and merge it into the current instance. Loads run serially, and concurrent requests for the same target share a promise:
import { createFeaturevisor } from '@featurevisor/sdk'export let f = createFeaturevisor({})const loadedTargets = new Set()const inFlight = new Map()let pending = Promise.resolve()let currentReleaseUrl = 'https://cdn.yoursite.com/releases/build-124'function enqueue(operation) { const result = pending.then(operation) pending = result.catch(() => {}) return result}async function fetchJson(url) { const response = await fetch(url, { signal: AbortSignal.timeout(10000), }) if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`) return response.json()}function mergeAccepted(instance, datafile) { if (!instance.setDatafile(datafile)) { throw new Error('SDK rejected a target datafile') }}function assertCompatible(instance, datafile) { if ( datafile.revision !== instance.getRevision() || datafile.schemaVersion !== instance.getSchemaVersion() ) { throw new Error('Target datafiles do not belong to one compatible release') }}export function loadTarget(target) { if (!/^[a-zA-Z0-9_-]+$/.test(target)) { return Promise.reject(new Error('Invalid target name')) } if (inFlight.has(target)) return inFlight.get(target) const request = enqueue(async () => { if (loadedTargets.has(target)) return const datafile = await fetchJson( `${currentReleaseUrl}/featurevisor-${target}.json`, ) if (loadedTargets.size) assertCompatible(f, datafile) mergeAccepted(f, datafile) loadedTargets.add(target) }).finally(() => inFlight.delete(target)) inFlight.set(target, request) return request}Await the target before evaluating. Before loading, a disabled or default result does not represent accepted configuration. Show an unavailable state if the first load fails.
import { f, loadTarget } from './featurevisor.js'// on the products pageawait loadTarget('products')const context = { userId: 'user-4', country: 'nl',}const showBanner = f.isEnabled('showMarketingBanner', context)Later, as the user navigates to another part of the application, load its datafile too. The features loaded earlier stay available:
import { f, loadTarget } from './featurevisor.js'// when the user reaches checkoutawait loadTarget('checkout')const context = { userId: 'user-4', country: 'nl',}// products and checkout features are now on the same instanceconst newCheckout = f.isEnabled('checkout', context)
Refresh the loaded targets#
To move to another release, fetch all loaded targets into a fresh instance before swapping it in. Append this function to your-app/featurevisor.js:
export function refreshLoadedTargets(releaseUrl) { return enqueue(async () => { const targets = [...loadedTargets] const datafiles = await Promise.all( targets.map((target) => fetchJson(`${releaseUrl}/featurevisor-${target}.json`), ), ) const candidate = createFeaturevisor({}) try { for (const [index, datafile] of datafiles.entries()) { if (index) assertCompatible(candidate, datafile) mergeAccepted(candidate, datafile) } } catch (error) { candidate.close() throw error } const previous = f f = candidate currentReleaseUrl = releaseUrl previous.close() })}Pass the next immutable release URL from your deployment system, then render again after the refresh resolves:
import { refreshLoadedTargets } from './featurevisor.js'try { await refreshLoadedTargets('https://cdn.yoursite.com/releases/build-125') // Reevaluate visible features and render with the current f binding.} catch (error) { console.warn('Configuration refresh failed; keeping the previous release', error)}The swap removes definitions absent from all refreshed targets. Replacing with only one target would drop the others, and merging into the old instance would keep definitions the new release deleted. Fetch or validation failures leave the previous instance, release URL and loaded target list unchanged, ready for a retry.
React to new features#
After a load or refresh, reevaluate using the current exported f, not a retained reference. Loads merge into that instance. Refresh events fire on the candidate before the swap. Notify your UI after the promise resolves.
Limits#
- This example passes context per call and does not migrate children, stored context, sticky maps, modules or subscriptions during a refresh. Use one complete target if you need stable instance identity.
- Retain immutable releases while clients use them, and never combine targets from different projects or build settings merely because their revisions match.
- Stop triggers and await pending operations before closing the instance, allow memory for both configurations during refresh, and adapt the timeout API for older browsers.
Related#
- Microfrontends, where each one loads its own datafile
- Targets
- JavaScript SDK: merge or replace

