> ## Documentation Index
> Fetch the complete documentation index at: https://blode.co/dnd-grid/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Local storage

Persist layout to local storage and restore on load.

<div className="not-prose my-6 rounded-lg border border-zinc-200/70 bg-white/70 shadow-sm dark:border-white/10 dark:bg-white/5">
  <iframe
    title="Local storage preview"
    src="https://blode.co/dnd-grid/examples/localstorage-example?embed=1"
    className="h-[640px] w-full"
    loading="lazy"
  />
</div>

[View source on GitHub](https://github.com/mblode/dnd-grid/blob/main/apps/web/examples/dnd-grid-localstorage-example.tsx)

## Installation

### CLI

```bash
npx shadcn@latest add https://blode.co/dnd-grid/r/localstorage-example.json
```
### Manual

```bash
npm install @dnd-grid/react
```

```css
@import "@dnd-grid/react/styles.css";
```

```tsx title="components/dnd-grid-localstorage-example.tsx"
"use client";

import { DndGrid, type Layout, layoutSchema } from "@dnd-grid/react";
import { useEffect, useState } from "react";

const STORAGE_KEY = "dnd-grid-layout";

const defaultLayout: Layout = [0, 1, 2, 3, 4, 5].map((i) => ({
  id: i.toString(),
  x: (i * 2) % 12,
  y: Math.floor(i / 6) * 2,
  w: 2,
  h: 2,
}));

const normalizeLayout = (value: unknown): Layout | null => {
  if (!Array.isArray(value)) {
    return null;
  }

  const normalized = value.map((item, index) => {
    if (!item || typeof item !== "object") {
      return null;
    }
    const record = item as Record<string, unknown>;
    const candidate = record.id ?? record.i ?? record.key;

    let id: string;
    if (typeof candidate === "string" && candidate.trim().length > 0) {
      id = candidate;
    } else if (typeof candidate === "number" && Number.isFinite(candidate)) {
      id = candidate.toString();
    } else {
      id = `item-${index}`;
    }

    return {
      ...record,
      id,
    };
  });

  if (normalized.some((item) => item === null)) {
    return null;
  }

  const parsed = layoutSchema.safeParse(normalized);
  return parsed.success ? (parsed.data as Layout) : null;
};

export function LocalStorageExample() {
  const [layout, setLayout] = useState<Layout>(defaultLayout);

  useEffect(() => {
    const saved = localStorage.getItem(STORAGE_KEY);
    if (!saved) {
      return;
    }
    try {
      const parsed = JSON.parse(saved);
      const normalized = normalizeLayout(parsed);
      if (normalized) {
        setLayout(normalized);
        return;
      }
    } catch {
      // ignore and fall back to default layout
    }
    setLayout(defaultLayout);
    localStorage.removeItem(STORAGE_KEY);
  }, []);

  const handleLayoutChange = (newLayout: Layout) => {
    setLayout(newLayout);
    localStorage.setItem(STORAGE_KEY, JSON.stringify(newLayout));
  };

  const resetLayout = () => {
    setLayout(defaultLayout);
    localStorage.removeItem(STORAGE_KEY);
  };

  return (
    <div>
      <button onClick={resetLayout} type="button">
        Reset Layout
      </button>

      <DndGrid
        cols={12}
        layout={layout}
        onLayoutChange={handleLayoutChange}
        rowHeight={50}
      >
        {layout.map((item) => (
          <div className="grid-item" key={item.id}>
            {item.id}
          </div>
        ))}
      </DndGrid>
    </div>
  );
}
```

## Usage

```tsx
import { LocalStorageExample } from "@/components/dnd-grid-localstorage-example";

export default function Page() {
  return <LocalStorageExample />;
}
```