WorkRunner Documentation

Data Persistence

Constructs use localStorage to persist data. Here's how to do it right.

Basic Pattern

Every Construct follows this pattern for data persistence:

// 1. Define a unique storage key with work-construct- prefix
const STORAGE_KEY = "work-construct-expense-tracker";

// 2. Initialize state from localStorage
const [expenses, setExpenses] = useState<Expense[]>(() => {
  // Check for window to handle SSR
  if (typeof window === "undefined") return [];

  const saved = localStorage.getItem(STORAGE_KEY);
  return saved ? JSON.parse(saved) : [];
});

// 3. Save to localStorage whenever state changes
useEffect(() => {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(expenses));
}, [expenses]);

Storage Key Rules

Always prefix with work-construct-

work-construct-todo-list

Use lowercase with hyphens

work-construct-expense-tracker

Make it descriptive

work-construct-inventory-manager

Don't use generic names

data, items, state

Complex State

For Constructs with multiple pieces of state, combine them into a single object:

interface AppState {
  expenses: Expense[];
  categories: string[];
  settings: {
    currency: string;
    showArchived: boolean;
  };
}

const STORAGE_KEY = "work-construct-expense-tracker";

const defaultState: AppState = {
  expenses: [],
  categories: ["Food", "Transport", "Utilities"],
  settings: {
    currency: "USD",
    showArchived: false,
  },
};

const [state, setState] = useState<AppState>(() => {
  if (typeof window === "undefined") return defaultState;

  const saved = localStorage.getItem(STORAGE_KEY);
  if (!saved) return defaultState;

  // Merge with defaults to handle schema changes
  const parsed = JSON.parse(saved);
  return { ...defaultState, ...parsed };
});

useEffect(() => {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}, [state]);

useReducer Pattern

For complex state logic, use useReducer:

type Action =
  | { type: "ADD_EXPENSE"; payload: Expense }
  | { type: "DELETE_EXPENSE"; payload: string }
  | { type: "UPDATE_SETTINGS"; payload: Partial<Settings> };

function reducer(state: AppState, action: Action): AppState {
  switch (action.type) {
    case "ADD_EXPENSE":
      return {
        ...state,
        expenses: [...state.expenses, action.payload],
      };
    case "DELETE_EXPENSE":
      return {
        ...state,
        expenses: state.expenses.filter((e) => e.id !== action.payload),
      };
    case "UPDATE_SETTINGS":
      return {
        ...state,
        settings: { ...state.settings, ...action.payload },
      };
    default:
      return state;
  }
}

// In component
const [state, dispatch] = useReducer(reducer, defaultState, (initial) => {
  if (typeof window === "undefined") return initial;
  const saved = localStorage.getItem(STORAGE_KEY);
  return saved ? { ...initial, ...JSON.parse(saved) } : initial;
});

useEffect(() => {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}, [state]);

Data Validation

Always validate data loaded from localStorage:

function loadState(): AppState {
  if (typeof window === "undefined") return defaultState;

  try {
    const saved = localStorage.getItem(STORAGE_KEY);
    if (!saved) return defaultState;

    const parsed = JSON.parse(saved);

    // Validate structure
    if (!Array.isArray(parsed.expenses)) {
      return defaultState;
    }

    // Validate each item
    const validExpenses = parsed.expenses.filter(
      (e: unknown) =>
        typeof e === "object" &&
        e !== null &&
        typeof (e as Expense).id === "string" &&
        typeof (e as Expense).amount === "number"
    );

    return {
      ...defaultState,
      ...parsed,
      expenses: validExpenses,
    };
  } catch {
    // If parsing fails, return defaults
    return defaultState;
  }
}

Storage Limits

Keep data small

localStorage has a 5MB limit per origin. Keep your data lean:

  • • Don't store large files or images
  • • Limit lists to reasonable sizes (e.g., 1000 items)
  • • Archive old data instead of keeping everything
  • • Consider implementing data cleanup

Clear Data Option

Consider adding a way for users to clear their data:

const clearAllData = () => {
  if (confirm("Are you sure? This will delete all your data.")) {
    localStorage.removeItem(STORAGE_KEY);
    setState(defaultState);
  }
};

// In settings or footer
<Button variant="destructive" onClick={clearAllData}>
  Clear All Data
</Button>