· Nacho Coll · Tutorials  · 10 Min. Lesezeit

IPFS + React: Eine dezentrale Datei-Upload-Komponente bauen

Bauen Sie eine React-Drag-and-Drop-Datei-Upload-Komponente, die Dateien an IPFS pinnt. Vollständiges Tutorial mit signierten Tokens für sichere Browser-Uploads.

Bauen Sie eine React-Drag-and-Drop-Datei-Upload-Komponente, die Dateien an IPFS pinnt. Vollständiges Tutorial mit signierten Tokens für sichere Browser-Uploads.

Dezentrale Anwendungen mit React und IPFS zu bauen war noch nie einfacher. In diesem umfassenden Tutorial lernen Sie, wie Sie eine produktionsreife Datei-Upload-Komponente erstellen, die Dateien direkt aus dem Browser an IPFS pinnt. Wir verwenden die signierten Tokens von IPFS.NINJA, um sichere Uploads zu gewährleisten, ohne API-Schlüssel in Ihrem Frontend-Code preiszugeben.

Am Ende dieser Anleitung haben Sie eine voll funktionsfähige Drag-and-Drop-Komponente mit Upload-Fortschritt, Fehlerbehandlung und ordentlichem State-Management mit TanStack Query.

IPFS Ninja

Warum eine React-IPFS-Upload-Komponente bauen?

Traditionelle Cloud-Storage-Dienste wie AWS S3 zentralisieren Ihre Daten und schaffen Vendor-Lock-in. Mit IPFS (InterPlanetary File System) werden Ihre Dateien über ein Peer-to-Peer-Netzwerk verteilt, wodurch sie widerstandsfähiger und zensurresistenter werden.

Hauptvorteile dieses Ansatzes:

  • Dezentrale Speicherung: Dateien werden über mehrere Knoten verteilt
  • Inhaltsadressierung: Jede Datei erhält eine eindeutige CID (Content Identifier)
  • Unveränderliche Links: Einmal hochgeladen, können Dateien nicht ohne Änderung der CID geändert werden
  • Globale Zugänglichkeit: Dateien sind von jedem IPFS-Gateway aus zugänglich
  • Sichere Uploads: Verwenden Sie signierte Tokens, statt API-Schlüssel preiszugeben

Wenn Sie neu in IPFS-Konzepten sind, sehen Sie sich unseren Leitfaden zu Was ist IPFS-Pinning an, bevor Sie in den Code eintauchen.

Voraussetzungen

Bevor wir beginnen, stellen Sie sicher, dass Sie Folgendes haben:

  • Grundlegende React-Kenntnisse
  • Node.js 16+ installiert
  • Ein IPFS.NINJA-Konto (hier anmelden)
  • Verständnis moderner React-Patterns (Hooks, async/await)

Projekt einrichten

Erstellen wir zunächst ein neues React-Projekt und installieren die benötigten Abhängigkeiten:

npx create-react-app ipfs-upload-demo
cd ipfs-upload-demo
npm install @tanstack/react-query react-dropzone lucide-react

Wir werden diese Bibliotheken verwenden:

  • @tanstack/react-query: Für State-Management und Caching
  • react-dropzone: Für Drag-and-Drop-Funktionalität
  • lucide-react: Für Icons

Signierte Tokens verstehen

Bevor wir die Komponente bauen, ist es wichtig zu verstehen, warum wir signierte Tokens statt API-Schlüssel verwenden:

// ❌ Tun Sie das niemals - das gibt Ihren API-Schlüssel preis
const response = await fetch('https://api.ipfs.ninja/upload/new', {
  headers: {
    'X-Api-Key': 'bws_your_secret_key_here' // Das ist im Browser sichtbar!
  }
});

// ✅ Verwenden Sie stattdessen signierte Tokens
const response = await fetch('https://api.ipfs.ninja/upload/new', {
  headers: {
    'Authorization': `Signed ${signedToken}` // Sicher für client-seitige Nutzung
  }
});

Signierte Tokens sind zeitlich begrenzte Anmeldedaten, die Sie serverseitig generieren und an Ihr Frontend übergeben. Sie können nicht verwendet werden, um auf Ihre Kontoeinstellungen oder andere sensible Operationen zuzugreifen.

Den Backend-Token-Endpunkt erstellen

Erstellen Sie zunächst einen einfachen Backend-Endpunkt zur Generierung signierter Tokens. Dies könnte eine Next.js-API-Route, ein Express-Endpunkt oder eine Serverless-Funktion sein:

// api/upload-token.js (Next.js-API-Route-Beispiel)
export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  try {
    const response = await fetch('https://api.ipfs.ninja/upload/tokens', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Api-Key': process.env.IPFS_NINJA_API_KEY // Nur serverseitig!
      },
      body: JSON.stringify({
        expiresIn: '1h',
        maxUploads: 10,
        description: 'React upload component token'
      })
    });

    const data = await response.json();
    
    if (!response.ok) {
      throw new Error(data.error || 'Token generation failed');
    }

    res.json({ token: data.token });
  } catch (error) {
    console.error('Token generation error:', error);
    res.status(500).json({ error: 'Failed to generate upload token' });
  }
}

Den Upload-Hook bauen

Erstellen wir einen benutzerdefinierten Hook zur Verwaltung der Upload-Logik mit TanStack Query:

// hooks/useIPFSUpload.js
import { useMutation, useQueryClient } from '@tanstack/react-query';

const uploadToIPFS = async ({ file, token }) => {
  // Datei für JSON-Upload in base64 konvertieren
  const base64Content = await new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result.split(',')[1]);
    reader.readAsDataURL(file);
  });

  const response = await fetch('https://api.ipfs.ninja/upload/new', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Signed ${token}`
    },
    body: JSON.stringify({
      content: base64Content,
      description: `Uploaded via React: ${file.name}`,
      metadata: {
        originalName: file.name,
        size: file.size,
        type: file.type,
        uploadedAt: new Date().toISOString()
      }
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error || 'Upload failed');
  }

  const data = await response.json();
  return {
    ...data,
    originalFile: {
      name: file.name,
      size: file.size,
      type: file.type
    }
  };
};

export const useIPFSUpload = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: uploadToIPFS,
    onSuccess: (data) => {
      // Hochgeladene Dateidaten cachen
      queryClient.setQueryData(['uploads'], (oldData = []) => [
        ...oldData,
        data
      ]);
    }
  });
};

Upload-Tokens abrufen

Erstellen Sie einen weiteren Hook, um signierte Tokens von Ihrem Backend abzurufen:

// hooks/useUploadToken.js
import { useQuery } from '@tanstack/react-query';

const fetchUploadToken = async () => {
  const response = await fetch('/api/upload-token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    }
  });

  if (!response.ok) {
    throw new Error('Failed to fetch upload token');
  }

  const data = await response.json();
  return data.token;
};

export const useUploadToken = () => {
  return useQuery({
    queryKey: ['upload-token'],
    queryFn: fetchUploadToken,
    staleTime: 30 * 60 * 1000, // 30 Minuten
    retry: 3
  });
};

Die Upload-Komponente erstellen

Bauen wir nun die Haupt-Upload-Komponente mit Drag-and-Drop-Funktionalität:

// components/IPFSUploader.jsx
import React, { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { Upload, CheckCircle, XCircle, Loader2, Copy, ExternalLink } from 'lucide-react';
import { useIPFSUpload } from '../hooks/useIPFSUpload';
import { useUploadToken } from '../hooks/useUploadToken';

const IPFSUploader = () => {
  const [uploadedFiles, setUploadedFiles] = useState([]);
  const uploadMutation = useIPFSUpload();
  const { data: token, isLoading: isTokenLoading, error: tokenError } = useUploadToken();

  const onDrop = useCallback(async (acceptedFiles) => {
    if (!token) {
      console.error('No upload token available');
      return;
    }

    for (const file of acceptedFiles) {
      try {
        const result = await uploadMutation.mutateAsync({ file, token });
        setUploadedFiles(prev => [...prev, result]);
      } catch (error) {
        console.error('Upload failed:', error);
      }
    }
  }, [token, uploadMutation]);

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    maxSize: 10 * 1024 * 1024, // 10MB-Limit
    multiple: true
  });

  const copyToClipboard = (text) => {
    navigator.clipboard.writeText(text);
  };

  const formatFileSize = (bytes) => {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  };

  if (isTokenLoading) {
    return (
      <div className="flex items-center justify-center p-8">
        <Loader2 className="w-6 h-6 animate-spin" />
        <span className="ml-2">Initializing uploader...</span>
      </div>
    );
  }

  if (tokenError) {
    return (
      <div className="p-4 bg-red-50 border border-red-200 rounded-lg">
        <div className="flex items-center">
          <XCircle className="w-5 h-5 text-red-500" />
          <span className="ml-2 text-red-700">Failed to initialize uploader</span>
        </div>
      </div>
    );
  }

  return (
    <div className="max-w-2xl mx-auto p-6">
      {/* Upload-Dropzone */}
      <div
        {...getRootProps()}
        className={`
          border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors
          ${isDragActive 
            ? 'border-blue-400 bg-blue-50' 
            : 'border-gray-300 hover:border-gray-400'
          }
        `}
      >
        <input {...getInputProps()} />
        <Upload className="w-12 h-12 mx-auto text-gray-400 mb-4" />
        {isDragActive ? (
          <p className="text-blue-600">Drop the files here...</p>
        ) : (
          <div>
            <p className="text-gray-600 mb-2">
              Drag & drop files here, or click to select
            </p>
            <p className="text-sm text-gray-500">
              Max file size: 10MB
            </p>
          </div>
        )}
      </div>

      {/* Upload-Fortschritt */}
      {uploadMutation.isPending && (
        <div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
          <div className="flex items-center">
            <Loader2 className="w-5 h-5 animate-spin text-blue-500" />
            <span className="ml-2 text-blue-700">Uploading to IPFS...</span>
          </div>
        </div>
      )}

      {/* Upload-Fehler */}
      {uploadMutation.isError && (
        <div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
          <div className="flex items-center">
            <XCircle className="w-5 h-5 text-red-500" />
            <span className="ml-2 text-red-700">
              {uploadMutation.error?.message || 'Upload failed'}
            </span>
          </div>
        </div>
      )}

      {/* Liste hochgeladener Dateien */}
      {uploadedFiles.length > 0 && (
        <div className="mt-6">
          <h3 className="text-lg font-semibold mb-4">Uploaded Files</h3>
          <div className="space-y-3">
            {uploadedFiles.map((file, index) => (
              <div key={index} className="p-4 bg-green-50 border border-green-200 rounded-lg">
                <div className="flex items-start justify-between">
                  <div className="flex items-start space-x-3">
                    <CheckCircle className="w-5 h-5 text-green-500 mt-0.5" />
                    <div>
                      <p className="font-medium text-gray-900">
                        {file.originalFile.name}
                      </p>
                      <p className="text-sm text-gray-500">
                        {formatFileSize(file.originalFile.size)} • {file.sizeMB}MB on IPFS
                      </p>
                      <div className="mt-2">
                        <p className="text-xs text-gray-600">CID:</p>
                        <div className="flex items-center space-x-2 mt-1">
                          <code className="text-xs bg-gray-100 px-2 py-1 rounded">
                            {file.cid}
                          </code>
                          <button
                            onClick={() => copyToClipboard(file.cid)}
                            className="text-gray-400 hover:text-gray-600"
                          >
                            <Copy className="w-4 h-4" />
                          </button>
                        </div>
                      </div>
                    </div>
                  </div>
                  <a
                    href={file.uris.url}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="text-blue-500 hover:text-blue-700"
                  >
                    <ExternalLink className="w-5 h-5" />
                  </a>
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

export default IPFSUploader;

Die App-Komponente einrichten

Zum Schluss wickeln Sie Ihre App mit dem TanStack-Query-Provider ein:

// App.js
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import IPFSUploader from './components/IPFSUploader';
import './App.css';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 1,
      staleTime: 5 * 60 * 1000, // 5 Minuten
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <header className="bg-white shadow-sm">
          <div className="max-w-4xl mx-auto px-4 py-6">
            <h1 className="text-3xl font-bold text-gray-900">
              IPFS File Uploader
            </h1>
            <p className="text-gray-600 mt-2">
              Upload files to IPFS with drag-and-drop simplicity
            </p>
          </div>
        </header>
        <main className="max-w-4xl mx-auto px-4 py-8">
          <IPFSUploader />
        </main>
      </div>
    </QueryClientProvider>
  );
}

export default App;

CSS-Styling hinzufügen

Fügen Sie einige grundlegende Styles hinzu, damit die Komponente poliert aussieht:

/* App.css */
.App {
  min-height: 100vh;
  background-color: #f9fafb;
}

/* Ordentliches File-Input-Styling sicherstellen */
input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

Erweiterte Funktionen und Optimierungen

Dateityp-Validierung

Erweitern Sie die Dropzone mit Dateityp-Einschränkungen:

const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({
  onDrop,
  maxSize: 10 * 1024 * 1024,
  accept: {
    'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp'],
    'application/pdf': ['.pdf'],
    'text/plain': ['.txt'],
  },
  multiple: true
});

// Dateiverweigerungs-Fehler anzeigen
{fileRejections.length > 0 && (
  <div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
    <p className="text-yellow-800 font-medium">Some files were rejected:</p>
    <ul className="mt-2 text-sm text-yellow-700">
      {fileRejections.map(({ file, errors }) => (
        <li key={file.path}>
          {file.path}: {errors.map(e => e.message).join(', ')}
        </li>
      ))}
    </ul>
  </div>
)}

Upload-Fortschritt verfolgen

Für eine bessere Nutzererfahrung möchten Sie vielleicht den Upload-Fortschritt einzelner Dateien verfolgen. Während die IPFS.NINJA-API keine Fortschritts-Callbacks direkt unterstützt, können Sie den Fortschritt simulieren oder den Upload-Status pro Datei anzeigen:

const [uploadingFiles, setUploadingFiles] = useState(new Set());

const onDrop = useCallback(async (acceptedFiles) => {
  if (!token) return;

  for (const file of acceptedFiles) {
    const fileId = `${file.name}-${file.size}`;
    setUploadingFiles(prev => new Set([...prev, fileId]));

    try {
      const result = await uploadMutation.mutateAsync({ file, token });
      setUploadedFiles(prev => [...prev, result]);
    } catch (error) {
      console.error('Upload failed:', error);
    } finally {
      setUploadingFiles(prev => {
        const next = new Set(prev);
        next.delete(fileId);
        return next;
      });
    }
  }
}, [token, uploadMutation]);

Best Practices für Fehlerbehandlung

Implementieren Sie umfassende Fehlerbehandlung für eine produktionsreife Komponente:

const getErrorMessage = (error) => {
  if (error.message.includes('401')) {
    return 'Upload token expired. Please refresh the page.';
  }
  if (error.message.includes('413')) {
    return 'File too large. Maximum size is 10MB.';
  }
  if (error.message.includes('429')) {
    return 'Too many uploads. Please wait a moment and try again.';
  }
  return error.message || 'Upload failed. Please try again.';
};

Ihre Komponente testen

So testen Sie die Upload-Funktionalität:

  1. Starten Sie Ihren Entwicklungsserver: npm start
  2. Ziehen Sie einige Dateien in den Upload-Bereich
  3. Überprüfen Sie, dass Dateien hochgeladen werden und CIDs angezeigt werden
  4. Verifizieren Sie, dass die IPFS-URLs funktionieren, indem Sie auf die externen Link-Icons klicken
  5. Testen Sie Fehlerszenarien (große Dateien, Netzwerkprobleme usw.)

Überlegungen für das Deployment

Beim Deployment Ihres React-IPFS-Uploaders:

Umgebungsvariablen

Speichern Sie Ihren IPFS.NINJA-API-Schlüssel sicher:

# .env.local (für Next.js)
IPFS_NINJA_API_KEY=bws_your_api_key_here

Token-Aktualisierungsstrategie

Implementieren Sie eine automatische Token-Aktualisierung für lang laufende Sitzungen:

export const useUploadToken = () => {
  return useQuery({
    queryKey: ['upload-token'],
    queryFn: fetchUploadToken,
    staleTime: 25 * 60 * 1000, // 25 Minuten (Tokens verfallen in 30)
    refetchOnWindowFocus: true,
    retry: 3
  });
};

CORS-Konfiguration

Stellen Sie sicher, dass Ihr Backend Anfragen von Ihrer Frontend-Domain zulässt.

Performance-Optimierung

Für bessere Performance bei großen Dateien oder mehreren Uploads:

  1. Verwenden Sie Web Workers für base64-Konvertierung
  2. Implementieren Sie eine Upload-Queue, um gleichzeitige Uploads zu begrenzen
  3. Fügen Sie Dateikomprimierung hinzu für Bilder vor dem Upload
  4. Cachen Sie Upload-Tokens ordentlich, um unnötige Anfragen zu vermeiden

Vergleich mit anderen Lösungen

Unser Ansatz mit IPFS.NINJA bietet mehrere Vorteile gegenüber Alternativen. Wenn Sie verschiedene IPFS-Pinning-Dienste evaluieren, schauen Sie sich unseren umfassenden Vergleich in IPFS.NINJA vs Pinata und unseren Leitfaden zu den besten IPFS-Pinning-Diensten an.

Für ein breiteres Verständnis der IPFS-Upload-Methoden siehe unser IPFS-Upload-API-Tutorial und unsere Anleitung Wie man Dateien zu IPFS hochlädt.

Nächste Schritte

Sie haben jetzt eine voll funktionsfähige React-IPFS-Upload-Komponente! Hier sind einige Möglichkeiten, sie zu erweitern:

  • Datei-Vorschau-Funktionen hinzufügen
  • Batch-Operationen implementieren (Löschen, Teilen)
  • Upload-Analytics und Metriken erstellen
  • Integration mit dem Authentifizierungssystem Ihrer App hinzufügen
  • Funktionen zur Ordnerorganisation bauen
  • Dateisuche und -filterung implementieren

Die Kombination aus Reacts Komponentenarchitektur und der dezentralen Speicherung von IPFS schafft mächtige Möglichkeiten, um widerstandsfähige, nutzerfreundliche Anwendungen zu bauen.

Bereit zum Pinnen? Erstellen Sie ein kostenloses Konto — 50 Dateien, 1 GB Speicher, 2 GB Bandbreite/Monat. Keine Kreditkarte erforderlich.

Zurück zum Blog

Verwandte Artikel

Alle Artikel anzeigen »