# Streambert Library Watchlist Data Persistence and Drag‑and‑Drop Reordering: A Complete Technical Guide

> Learn how to implement Streambert library watchlist data persistence and drag and drop reordering using localStorage and React refs. Keep your changes saved locally without API calls.

- Repository: [true_lock/streambert](https://github.com/truelockmc/streambert)
- Tags: deep-dive
- Published: 2026-05-21

---

**Streambert persists watchlist data to the browser's `localStorage` using a centralized storage utility and supports drag‑and‑drop reordering through React refs that update a `savedOrder` array, ensuring all changes are instantly written to local storage without requiring API calls.**

Streambert is an open-source streaming interface that manages user watchlists entirely client-side. The library implements a robust persistence layer for watchlist data and intuitive drag‑and‑drop reordering capabilities, storing all state locally in the browser. Understanding these mechanisms is essential for developers looking to customize watchlist behavior or integrate Streambert's local-first architecture into their own React applications.

## How Streambert Persists Watchlist Data

### The Storage Utility in [`src/utils/storage.js`](https://github.com/truelockmc/streambert/blob/main/src/utils/storage.js)

All watchlist persistence flows through a thin wrapper around the Web Storage API defined in **[`src/utils/storage.js`](https://github.com/truelockmc/streambert/blob/main/src/utils/storage.js)**. This utility namespaces every key with `streambert_` to avoid collisions with other applications sharing the same origin.

The storage object exposes `get` and `set` methods that handle JSON serialization and error suppression. When writing watchlist changes, the `set` method automatically encodes values and writes them to `localStorage` (lines 5‑18):

```javascript
// src/utils/storage.js
const PREFIX = "streambert_";

export const storage = {
  get(key) {
    try {
      const raw = localStorage.getItem(PREFIX + key);
      return raw ? JSON.parse(raw) : null;
    } catch {
      return null;
    }
  },
  set(key, value) {
    try {
      localStorage.setItem(PREFIX + key, JSON.stringify(value));
    } catch {}
  }
};

```

Two primary keys manage the watchlist state:

- **`saved`**: A serialized object mapping title IDs to boolean values, representing the user's saved titles.
- **`savedOrder`**: An optional array that stores the user‑defined sequence of titles, enabling custom ordering beyond insertion time.

### Watchlist State Management in [`App.jsx`](https://github.com/truelockmc/streambert/blob/main/App.jsx)

The main application component in **[`src/App.jsx`](https://github.com/truelockmc/streambert/blob/main/src/App.jsx)** initializes the watchlist state by hydrating from `localStorage` on mount (lines 55‑56). It uses lazy initialization with `useState` to read from storage only once during component construction:

```javascript
// src/App.jsx
const [saved, setSaved] = useState(() => storage.get(STORAGE_KEYS.SAVED) || {});
const [savedOrder, setSavedOrder] = useState(() => storage.get("savedOrder") || null);

```

When users add or remove titles, the application updates both React state and local storage synchronously. The `addToWatchlist` function (lines 674‑690) merges the new title into the existing saved object and immediately persists the change:

```javascript
// Adding a title
const addToWatchlist = (id) => {
  const newSaved = { ...saved, [id]: true };
  setSaved(newSaved);
  storage.set(STORAGE_KEYS.SAVED, newSaved);
  showToast("Added to watchlist");
};

// Removing a title
const removeFromWatchlist = (id) => {
  const { [id]: _, ...rest } = saved;
  setSaved(rest);
  storage.set(STORAGE_KEYS.SAVED, rest);
  showToast("Removed from watchlist");
};

```

Every mutation triggers `storage.set()`, ensuring the watchlist data persistence remains consistent across browser sessions and tab refreshes.

## Implementing Drag‑and‑Drop Reordering

Streambert supports two distinct drag‑and‑drop interactions that modify the watchlist ordering: home‑page row arrangement and saved‑item list ordering. Both implementations rely on React refs to track the dragged element and update the order array in‑place before persisting.

### Home‑Page Row Arrangement in [`SettingsPage.jsx`](https://github.com/truelockmc/streambert/blob/main/SettingsPage.jsx)

Users can customize which content rows appear on the Home page and reorder them via drag‑and‑drop. This logic resides in **[`src/pages/SettingsPage.jsx`](https://github.com/truelockmc/streambert/blob/main/src/pages/SettingsPage.jsx)** (lines 696‑710), where the component maintains `dragItem` and `dragOver` refs to track the source and target indices:

```javascript
// src/pages/SettingsPage.jsx
const dragItem = useRef(null);
const dragOver = useRef(null);

const handleDragStart = (e, idx) => {
  dragItem.current = idx;
};

const handleDragEnter = (e, idx) => {
  dragOver.current = idx;
};

const handleDrop = () => {
  const newOrder = [...homeRowOrder];
  const dragged = newOrder.splice(dragItem.current, 1)[0];
  newOrder.splice(dragOver.current, 0, dragged);
  setHomeRowOrder(newOrder);
  storage.set("homeRowOrder", newOrder);
  dragItem.current = null;
  dragOver.current = null;
};

```

The UI explicitly indicates this functionality with the instruction: *"Choose which rows appear on the Home page and drag to reorder them"* (line 739).

### Saved‑Item List Ordering in [`Sidebar.jsx`](https://github.com/truelockmc/streambert/blob/main/Sidebar.jsx)

Within the Sidebar component, users can reorder individual saved titles by dragging thumbnails. The implementation in **[`src/components/Sidebar.jsx`](https://github.com/truelockmc/streambert/blob/main/src/components/Sidebar.jsx)** (lines 28‑67) uses three tracking variables: `dragItem` (source index), `dragNode` (DOM reference), and `dragOver` (target index):

```javascript
// src/components/Sidebar.jsx
const dragItem = useRef(null);
const dragNode = useRef(null);
const [dragOver, setDragOver] = useState(null);

const handleDragStart = (e, index) => {
  dragItem.current = index;
  dragNode.current = e.currentTarget;
  dragNode.current.style.opacity = "0.4";
};

const handleDragEnter = (e, index) => {
  setDragOver(index);
};

const handleDragEnd = () => {
  if (dragNode.current) dragNode.current.style.opacity = "1";
  const newOrder = [...savedOrder];
  const dragged = newOrder.splice(dragItem.current, 1)[0];
  newOrder.splice(dragOver, 0, dragged);
  setSavedOrder(newOrder);
  storage.set("savedOrder", newOrder);
  dragItem.current = null;
  dragNode.current = null;
  setDragOver(null);
};

```

Each saved thumbnail renders with the `draggable` attribute, and the component applies a visual highlight when an item is dragged over a valid drop target (lines 145‑167). When the drag operation completes, the code rebuilds the `savedOrder` array and writes it to `localStorage`, ensuring the Streambert watchlist data persistence reflects the new sequence immediately.

## Backup and Restore Functionality

Streambert includes a complete backup and restore system in **[`src/pages/SettingsPage.jsx`](https://github.com/truelockmc/streambert/blob/main/src/pages/SettingsPage.jsx)** (lines 30‑84) that exports the entire watchlist state to a JSON file. This captures both the `saved` object and `savedOrder` array:

```javascript
// Exporting watchlist data
const handleExport = () => {
  const data = {
    data: {
      saved: storage.get(STORAGE_KEYS.SAVED),
      savedOrder: storage.get("savedOrder")
    }
  };
  const blob = new Blob([JSON.stringify(data, null, 2)], {
    type: "application/json"
  });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "streambert-backup.json";
  a.click();
  URL.revokeObjectURL(url);
};

```

The import function reads the JSON file, validates the `data` field, and restores each key via `storage.set()`, enabling users to migrate their watchlists across devices or browsers while preserving custom drag‑and‑drop ordering.

## Summary

- **Streambert** uses a centralized storage utility in [`src/utils/storage.js`](https://github.com/truelockmc/streambert/blob/main/src/utils/storage.js) to namespace and serialize watchlist data to `localStorage` under the `streambert_` prefix.
- The watchlist state consists of two structures: a `saved` object mapping title IDs and an optional `savedOrder` array for custom sequencing.
- Drag‑and‑drop reordering is implemented in [`Sidebar.jsx`](https://github.com/truelockmc/streambert/blob/main/Sidebar.jsx) (for saved titles) and [`SettingsPage.jsx`](https://github.com/truelockmc/streambert/blob/main/SettingsPage.jsx) (for home‑page rows) using React refs to track indices and `storage.set()` to persist changes.
- All persistence operations are synchronous and client‑side, requiring no network requests or external APIs.
- The backup system exports both data structures to JSON, allowing full restoration of watchlist state including custom ordering.

## Frequently Asked Questions

### How does Streambert store watchlist data without a backend?

Streambert writes all watchlist state to the browser's `localStorage` using a wrapper utility in [`src/utils/storage.js`](https://github.com/truelockmc/streambert/blob/main/src/utils/storage.js). This approach stores data locally on the user's device under the `streambert_` namespace, eliminating the need for server-side persistence while ensuring data survives page refreshes.

### What is the difference between the `saved` and `savedOrder` keys?

The `saved` key contains an object mapping title IDs to boolean values, indicating which titles are in the watchlist. The `savedOrder` key contains an array that defines the specific sequence in which those titles should appear. When `savedOrder` exists, Streambert uses it to render the watchlist; otherwise, it falls back to default ordering.

### Can I programmatically reorder the watchlist without using the drag‑and‑drop UI?

Yes. You can manipulate the `savedOrder` array directly using the storage utility and update the React state accordingly. Retrieve the current order with `storage.get("savedOrder")`, modify the array to your desired sequence, then persist it with `storage.set("savedOrder", newOrder)` and call `setSavedOrder(newOrder)` to update the UI.

### Where is the drag‑and‑drop logic implemented in the Streambert codebase?

The library implements two drag‑and‑drop systems: home‑page row reordering lives in [`src/pages/SettingsPage.jsx`](https://github.com/truelockmc/streambert/blob/main/src/pages/SettingsPage.jsx) (lines 696‑710), while saved‑item reordering within the sidebar is handled in [`src/components/Sidebar.jsx`](https://github.com/truelockmc/streambert/blob/main/src/components/Sidebar.jsx) (lines 28‑67). Both use native HTML5 drag‑and‑drop events coupled with React refs to track and persist ordering changes.