Skip to content

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 with useUserState.
  • 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 the useAuth() sign-in gate in App.
  • 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

  1. The app shows "No notes yet." for a fresh user.
  2. Type a note into the input and press Enter or click Add. The note appears in the list.
  3. Add two or three more notes.
  4. Click Remove next to one note. It disappears from the list.
  5. Reload the page. All remaining notes are still present.
  6. 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 }. value is always a string[] 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