Control an app banner from the CLI
Build a small React app that shows an announcement banner and other client-visible values stored in SkyState public state, and change them from the terminal without a deploy. You will scaffold a Vite app, wire SkyStateProvider, read a multi-field object with usePublicState, and verify the app by pushing updates with the CLI.
Estimated time: 12 minutes.
What You'll Build
You will build a tutorial-app React project with a banner and other app values controlled by SkyState public state:
appConfig.bannercontrols a public announcement banner shown to every visitor.appConfig.supportcontrols support copy shown to every visitor.appConfig.limitscontrols a client-visible product limit.- The app uses a local fallback value so it can render before public state loads.
- A no-arg
usePublicState()gate shows a loading indicator and error message while SkyState connects.
Prerequisites
Before you start, make sure you have:
- Node.js 20 or later (required by the
skystateCLI). - npm installed.
- A SkyState account.
- A terminal and browser.
Step 1: Scaffold a React App
Create a new Vite React app:
bash
npm create vite@latest tutorial-app -- --template react-tsMove into the app directory and install its dependencies:
bash
cd tutorial-app
npm installStart the Vite dev server on the tutorial's fixed local port:
bash
npm run dev -- --port 5173 --strictPortOpen http://localhost:5173/. Leave the dev server running while you make the code changes in the next steps.
Step 2: Install the React SDK
bash
npm install @skystate/reactThe React package provides SkyStateProvider, usePublicState, and the other hooks. It depends on @skystate/core, so you do not need to install the core package separately for this React tutorial.
Step 3: Set Up the CLI and Project
Install the CLI globally if you have not already:
bash
npm install -g skystateSign in:
bash
sky loginCreate the tutorial project. Project slugs are globally unique across all SkyState accounts, so choose a slug that is specific to you (for example, append your username or a random suffix):
bash
sky project create tutorial-app your-tutorial-appReplace your-tutorial-app with the slug you choose. Use that same slug in every later command in this tutorial that includes --project.
Get your account ID:
bash
sky statusIn the sky status output, find the Account section and copy the SLUG value. This is the acc_... account ID the SDK provider needs.
You can also create and inspect projects in the SkyState console, but this tutorial uses the CLI path.
Step 4: Read the Banner and Other App Values
Open src/App.tsx and replace its contents with this code:
tsx
import { usePublicState } from '@skystate/react';
type AppConfig = {
banner: {
visible: boolean;
message: string;
};
support: {
label: string;
email: string;
};
limits: {
maxSavedViews: number;
};
};
const fallbackConfig: AppConfig = {
banner: {
visible: false,
message: '',
},
support: {
label: 'Email support',
email: 'support@example.com',
},
limits: {
maxSavedViews: 3,
},
};
function AppContent() {
const { value: config } = usePublicState<AppConfig>('appConfig', fallbackConfig);
return (
<main>
{config.banner.visible && (
<div className="banner">{config.banner.message}</div>
)}
<h1>Tutorial app</h1>
<section className="config-panel">
<p>{config.support.label}</p>
<a href={`mailto:${config.support.email}`}>
{config.support.email}
</a>
<p>Saved views: {config.limits.maxSavedViews}</p>
</section>
</main>
);
}
export function App() {
const status = usePublicState();
if (status.status === 'loading' || status.status === 'idle') {
return <p>Loading config…</p>;
}
if (status.status === 'error') {
return <p>Could not load config: {status.error.message}</p>;
}
return <AppContent />;
}The no-arg usePublicState() call returns the overall public-state status. Checking status before rendering AppContent ensures the app handles the loading and error states explicitly. Once status is 'ready', AppContent reads the appConfig key with a fallback, so config is always non-null.
Add a little styling in src/index.css if you want the config to stand out:
css
main {
max-width: 42rem;
margin: 4rem auto;
padding: 0 1rem;
}
.banner {
margin-bottom: 1rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
background: #123524;
color: #d6f7df;
font-weight: 600;
}
.config-panel {
display: grid;
gap: 0.5rem;
margin-top: 1.5rem;
}App is exported by name here. The next step's main.tsx snippet imports it as { App } to match.
Step 5: Wire the Provider
Open src/main.tsx and replace its contents with this code. Replace YOUR_ACCOUNT_ID with the acc_... account route identifier from sky status, and replace your-tutorial-app with the project slug you chose in the setup step.
tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { SkyStateProvider } from '@skystate/react';
import { App } from './App';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<SkyStateProvider
account="YOUR_ACCOUNT_ID"
project="your-tutorial-app"
environment="development"
>
<App />
</SkyStateProvider>
</StrictMode>,
);SkyStateProvider connects this React app to one SkyState account, project, and environment. This tutorial uses the development environment so you can test changes without affecting production state. The provider connects to the hosted SkyState API automatically. The ./index.css import keeps the scaffold's stylesheet loaded so the .banner and .config-panel rules from Step 4 take effect.
Verify
Check the fallback state
With the Vite dev server still running, open http://localhost:5173/. The page should show Tutorial app, the fallback support email, and no banner because the fallback has banner.visible: false.
The loading indicator may flash briefly before the config loads.
Push new values and watch the app update
Push public state with the CLI. The --comment flag records why this version was created:
bash
sky state public push \
--json '{"appConfig":{"banner":{"visible":true,"message":"Maintenance window Friday at 18:00 UTC."},"support":{"label":"Priority support","email":"help@example.com"},"limits":{"maxSavedViews":5}}}' \
--project your-tutorial-app \
--env development \
--comment "Enable maintenance banner"Reload the browser page. The app should now show the maintenance banner, priority support copy, and a saved-views limit of 5.
Update config again
To update the config again, push a new value and reload:
bash
sky state public push \
--json '{"appConfig":{"banner":{"visible":false,"message":""},"support":{"label":"Standard support","email":"support@example.com"},"limits":{"maxSavedViews":3}}}' \
--project your-tutorial-app \
--env development \
--comment "Disable maintenance banner"Reload the page. The banner is gone and the support copy shows the standard defaults again. Development reads are cached for 10 seconds, so if you reload immediately after pushing you may still see the old value. Wait a moment and refresh again if needed.
Breaking-change confirmation
If you push a config that removes a top-level key or changes a top-level value's type (from a number to a string, for example), the CLI detects the breaking change and asks for confirmation before writing. Push a second top-level key to see this in action:
bash
sky state public push \
--json '{"appConfig":{"banner":{"visible":false,"message":""},"support":{"label":"Standard support","email":"support@example.com"},"limits":{"maxSavedViews":3}},"debugMode":true}' \
--project your-tutorial-app \
--env development \
--comment "Add debug mode flag"Now remove debugMode. Because removing a top-level key is a breaking change, the CLI prompts before writing. Pass --yes to skip the prompt in scripts:
bash
sky state public push \
--json '{"appConfig":{"banner":{"visible":false,"message":""},"support":{"label":"Standard support","email":"support@example.com"},"limits":{"maxSavedViews":3}}}' \
--project your-tutorial-app \
--env development \
--yesThe CLI checks for top-level key removals and type changes. Replacing one object with another object at the same key is not flagged as breaking, because the type (object) stays the same. Removing a key or changing a top-level number to a string triggers the confirmation.
This is the remote-control loop: edit JSON in the terminal, push, reload, see the change. No deploy needed.
What You Learned
You created a SkyState project from the CLI, found your account ID with sky status, mounted SkyStateProvider in a React app, read a multi-field object with usePublicState, handled the loading and error states, and updated the banner from the terminal with sky state public push.
The same pattern works for any public values that can be read by any visitor: announcement banners, feature flags, support copy, safe client-side limits, and theme settings.
Where to Go Next
- Continue with Tutorial 2: Save theme preferences with toggles.
- Read the
usePublicStatereference.