Save notes for each user
This tutorial adds a per-user notes list to the tutorial-app. Each signed-in user gets their own independent list of notes stored in SkyState user state.
Estimated time: 20 minutes.
What You'll Build
You will extend the tutorial-app with a notes list that:
- Reads a
string[]user-state key withuseUserState. - Appends a new note using the functional updater form of
set. - Removes a note by index using the functional updater form of
set. - Keeps each user's notes completely independent from other users' notes.
Prerequisites
Before you start, make sure you have:
- Completed Tutorial 2: Save theme preferences with toggles. You need the provider configured with
callbackUrl, end-user auth enabled on the project, and theuseAuth()sign-in gate inApp. - The Vite dev server running at
http://localhost:5173/(npm run dev -- --port 5173 --strictPort).
Step 1: Add the notes state hook
The notes key holds a string[] value. The fallback [] gives the UI an empty list before the user has saved anything.
Inside the authenticated branch of App, add a Notes component. Keep the full auth gate from Tutorial 2 and add the new component:
tsx
import { useAuth, usePublicState } from '@skystate/react';
import { Notes } from './Notes';
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 Banner() {
const { value: config } = usePublicState<AppConfig>('appConfig', fallbackConfig);
if (!config.banner.visible) return null;
return <div className="banner">{config.banner.message}</div>;
}
export function App() {
const auth = useAuth();
if (auth.status === 'authenticating') {
return <p>Signing in...</p>;
}
if (auth.status === 'unauthenticated') {
return (
<main>
<Banner />
<p>Sign in to see your notes.</p>
<button type="button" onClick={() => { void auth.loginWithRedirect(); }}>
Sign in
</button>
</main>
);
}
return (
<main>
<Banner />
<Notes />
<button type="button" onClick={() => { void auth.logout(); }}>
Sign out
</button>
</main>
);
}Step 2: Build the Notes component
Create src/Notes.tsx:
tsx
import { useState } from 'react';
import { useUserState } from '@skystate/react';
export function Notes() {
const { value: notes, set: setNotes } = useUserState<string[]>('notes', []);
const [input, setInput] = useState('');
function addNote() {
const text = input.trim();
if (!text) return;
setNotes((prev) => [...prev, text]);
setInput('');
}
function removeNote(index: number) {
setNotes((prev) => prev.filter((_, i) => i !== index));
}
return (
<section>
<h2>Notes</h2>
<div>
<input
type="text"
value={input}
onChange={(e) => setInput(e.currentTarget.value)}
onKeyDown={(e) => { if (e.key === 'Enter') addNote(); }}
placeholder="Type a note and press Enter"
/>
<button type="button" onClick={addNote}>
Add
</button>
</div>
{notes.length === 0 ? (
<p>No notes yet.</p>
) : (
<ul>
{notes.map((note, index) => (
<li key={index}>
{note}
<button
type="button"
onClick={() => removeNote(index)}
aria-label={`Remove note: ${note}`}
>
Remove
</button>
</li>
))}
</ul>
)}
</section>
);
}The set function from useUserState accepts a functional updater that receives the previous committed value and returns the next value. This pattern avoids reading stale closure values when multiple updates arrive in quick succession.
Appending a note:
tsx
setNotes((prev) => [...prev, text]);Removing a note by index:
tsx
setNotes((prev) => prev.filter((_, i) => i !== index));Both calls save immediately. Each user's notes array is independent. Sign in as a different user and you see that user's notes.
Step 3: Run the app
The Vite dev server picks up the new file automatically. Open http://localhost:5173/. Sign in if prompted.
Verify
- The app shows "No notes yet." for a fresh user.
- Type a note into the input and press Enter or click Add. The note appears in the list.
- Add two or three more notes.
- Click Remove next to one note. It disappears from the list.
- Reload the page. All remaining notes are still present.
- Click Sign out and sign in as a different user. That user sees their own notes list (empty if they have never added any).
What You Learned
useUserState<string[]>('notes', [])returns{ value, set, draft }.valueis always astring[]because the fallback is provided.- The functional updater form
set((prev) => ...)is the safe pattern for array mutations. It receives the current committed value so the update is always based on the latest state. - Appending:
set((prev) => [...prev, item]). Removing:set((prev) => prev.filter(...)). - Each signed-in user has an independent copy of every user-state key. Sign in as a different user to confirm isolation.
Where to Go Next
- Tutorial 4: Build a user settings form. It uses the draft API to stage edits before saving.
- Reference: useUserState