useProjectConfig
useProjectConfig reads a value from the remote config and subscribes to changes at that path. When the value changes on the server and the SDK re-fetches, the component re-renders automatically.
Must be called inside a component that is a descendant of SkyStateProvider. Throws an error if called outside a provider.
Signatures
// With fallback — data is T (never undefined)
function useProjectConfig<T>(
path: string,
fallback: T,
): { data: T; isLoading: boolean; error: Error | null }
// Without fallback — data is T | undefined
function useProjectConfig<T = unknown>(
path?: string,
): { data: T | undefined; isLoading: boolean; error: Error | null }Return Value
{
data: T; // Current value (or fallback if key is missing)
isLoading: boolean; // true until the first fetch (success or failure) completes
error: Error | null; // Last error, or null if the most recent fetch succeeded
}data
The current value at path in the config, cast to T. If the key does not exist and a fallback was provided, data returns the fallback value. If the key does not exist and no fallback was provided, data is undefined.
The value is updated automatically when the server config changes and the SDK re-fetches.
isLoading
true from the initial render until the first fetch (successful or failed) completes. Use this to avoid showing stale or default UI before the server config is known.
After the first fetch, isLoading is always false, even during subsequent background re-fetches.
error
The most recent error from the HTTP client, or null if the last fetch succeeded. Errors are instances of SkyStateError or standard Error.
The component continues to render with the last known value (or fallback) when an error occurs — errors do not clear the cache.
Usage Examples
Boolean feature flag
import { useProjectConfig } from '@skystate/react';
export function NewCheckoutFlow() {
const { data: enabled, isLoading } = useProjectConfig<boolean>(
'features.newCheckout',
false,
);
if (isLoading) return null;
return enabled ? <NewCheckout /> : <LegacyCheckout />;
}String value (UI copy)
export function AnnouncementBanner() {
const { data: enabled } = useProjectConfig<boolean>('banner.enabled', true);
const { data: text } = useProjectConfig<string>('banner.text', 'Welcome!');
const { data: color } = useProjectConfig<string>('banner.color', '#3b82f6');
if (!enabled) return null;
return (
<div style={{ backgroundColor: color, color: '#fff' }}>
{text}
</div>
);
}Number value (limit)
export function WidgetList() {
const { data: maxItems } = useProjectConfig<number>('features.maxItems', 5);
return (
<ul>
{ALL_WIDGETS.slice(0, maxItems).map(widget => (
<li key={widget.id}>{widget.name}</li>
))}
</ul>
);
}Maintenance mode overlay
export function App() {
const { data: maintenanceEnabled } = useProjectConfig<boolean>(
'maintenance.enabled',
false,
);
return (
<>
{maintenanceEnabled && <MaintenanceOverlay />}
<main>...</main>
</>
);
}Reading the entire config object
Pass an empty string (or no path) to read the entire config as an object:
// Read entire config
const { data: allConfig } = useProjectConfig<Record<string, unknown>>();
// Equivalent with explicit empty string
const { data: allConfig } = useProjectConfig<Record<string, unknown>>('');Handling loading and errors explicitly
export function PricingBadge() {
const { data: badge, isLoading, error } = useProjectConfig<string>(
'pricing.badge',
'Popular',
);
if (isLoading) return <Skeleton />;
if (error) return null; // Hide badge on error, show nothing
return <span className="badge">{badge}</span>;
}Dot-path Notation
The path argument uses dots to navigate the config object hierarchy:
{
"banner": {
"enabled": true,
"text": "Hello!"
},
"features": {
"maxItems": 5
}
}| path | data |
|---|---|
'banner.enabled' | true |
'banner.text' | 'Hello!' |
'features.maxItems' | 5 |
'banner' | { enabled: true, text: 'Hello!' } |
'' or no argument | The entire config object |
Auto-registration in Development
When all of the following are true, the SDK automatically creates a missing key with the fallback value:
- The resolved environment is
'development' - A
devKeyis present (set viaSKYSTATE_DEV_KEY) - A
fallbackwas provided touseProjectConfig - The key does not exist in the current config
- The initial load has completed
// In development: if 'banner.enabled' does not exist in SkyState,
// the SDK creates it with the value `true`.
const { data: enabled } = useProjectConfig<boolean>('banner.enabled', true);In production (no devKey), the SDK logs a warning but returns the fallback normally.
See Auto-registration for full details.
TypeScript Notes
The generic parameter T is used to type data. The SDK does not perform runtime type checking — the cast is purely at the TypeScript level. If the server returns a value of a different type than expected, TypeScript will not catch it at runtime.
// TypeScript knows data is boolean (not boolean | undefined) because a fallback was provided
const { data } = useProjectConfig<boolean>('feature.enabled', false);
// ^? boolean
// Without fallback, data is T | undefined
const { data } = useProjectConfig<boolean>('feature.enabled');
// ^? boolean | undefinedFor object-typed config values, use an interface:
interface BannerConfig {
enabled: boolean;
text: string;
color: string;
}
const { data: banner } = useProjectConfig<BannerConfig>('banner', {
enabled: true,
text: 'Hello!',
color: '#3b82f6',
});Render Behaviour
Each call to useProjectConfig creates two subscriptions on the underlying ConfigStore:
- A subscription to
pathfor the value itself. - A subscription to
'__status'forisLoadinganderror.
The component only re-renders when the value at path (or one of its descendants) changes, or when the loading/error status changes. Changes to unrelated config paths do not trigger a re-render.
Object references are stable when values have not changed. If the server returns a config where banner.text changed but features.maxItems did not, the features object has the same reference as before.