Praktyczny przewodnik krok po kroku, jak stworzyć aplikację typu Todo z wykorzystaniem Vite, Firebase i TailwindCSS. Naucz się tworzyć funkcjonalne i estetyczne interfejsy webowe od podstaw.
W tym poradniku krok po kroku zbudujemy prostą aplikację TODO z użyciem React, Firebase i Firestore. Nauczysz się tworzyć projekt w Firebase, konfigurować Firestore, integrować bazę danych z aplikacją front-endową i obsługiwać operacje CRUD (tworzenie, odczyt, aktualizacja i usuwanie zadań).
Przejdź na oficjalna strona Firebase: https://firebase.google.com/.
Wejdź do konsoli Firebase, klikając “Get started in console”.
Kliknij “Get started with a Firebase project”.
Wprowadź nazwę swojego projektu, np. react-firebase-todo-app
Wyłącz funkcję Gemini AI (opcjonalnie).
Wyłącz Google Analytics (opcjonalnie) i kliknij “Create project”.
Po utworzeniu projektu kliknij “Continue”
Kliknij ikonę </>
(Web), aby dodać aplikację webową.
Nazwij aplikację, np. React Firebase Todo App
i kliknij “Register app”.
Skopiuj kod inicjalizujący Firebase i kliknij “Continue to console”.
W lewym panelu kliknij “Firestore Database”
Kliknij “Create database”.
Kliknij przycisk “Next”.
Wybierz “Start in test mode” i kliknij “Create”.
Utwórz projekt z Vite z szablonem React:
pnpm create vite firebase-react-todo-app --template react
# lub:
npm create vite firebase-react-todo-app --template react
Przejdź do folderu projektu:
cd firebase-react-todo-app
Zainstaluj zależności:
pnpm i
# lub:
npm i
Zainstaluj TailwindCSS:
pnpm add tailwindcss @tailwindcss/vite
# lub:
npm i tailwindcss @tailwindcss/vite
Zainstaluj Firebase:
pnpm add firebase
# lub:
npm i firebase
Otwórz projekt w VS Code:
code .
Usuń zbędne pliki starterowe od Vite
Usuń zbędne pliki:
src/App.css
src/assets/
Skonfiguruj TailwindCSS w vite.config.js
:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
});
Zaimportuj Tailwind w globalnym pliku CSS:
@import "tailwindcss";
Stwórz prosty interfejs użytkownika w pliku App.jsx
:
export default function App() {
return (
<div className="h-screen grid place-content-center">
<div className="w-96">
<h1 className="text-4xl font-bold text-center">Zadania do zrobienia</h1>
<div className="flex gap-1.5 my-4">
<input
placeholder="np. Pójdź na siłownie..."
className="outline-none border-2 px-2.5 py-1.5 rounded-md w-full"
/>
<button className="bg-black text-white hover:bg-black/80 transition-colors px-2.5 font-medium cursor-pointer rounded-md">
Dodaj
</button>
</div>
<div className="font-medium">Tutaj będą zadania</div>
</div>
</div>
);
}
Uruchom serwer deweloperski:
pnpm dev
# lub:
npm dev
Stwórz folder lib
w src
, a w nim plik firebase.js
.
Wklej kod inicjalizujący Firebase:
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: "...",
authDomain: "...",
projectId: "...",
storageBucket: "...",
messagingSenderId: "...",
appId: "...",
};
const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
Zaimportuj potrzebne funkcje i ustaw stany:
import { useState } from "react";
import { addDoc, collection } from "firebase/firestore";
import { db } from "./lib/firebase";
export default function App() {
const [todo, setTodo] = useState("");
const handleAddTodo = async () => {
try {
const newTodo = { todo, done: false };
setTodo("");
await addDoc(collection(db, "todos"), newTodo);
} catch (err) {
console.error(err);
}
};
return (
...
);
}
Podłącz onChange
do inputa oraz onClick
do przycisku:
<input
value={todo}
onChange={(e) => setTodo(e.target.value)}
...
/>
<button onClick={handleAddTodo}>Dodaj</button>
Zaimportuj useEffect
i getDocs
:
import { useEffect } from "react";
import { getDocs } from "firebase/firestore";
Stwórz stan todos
i funkcję fetchTodos
:
const [todos, setTodos] = useState([]);
const fetchTodos = async () => {
try {
const snapshot = await getDocs(collection(db, "todos"));
const data = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
setTodos(data);
} catch (err) {
console.error(err);
}
};
useEffect(() => {
fetchTodos();
}, []);
Wyświetl zadanie
<div className="font-medium flex flex-col gap-1.5">
{todos.map((todo) => (
<p key={todo.id}>{todo.todo}</p>
))}
</div>
Zaimportuj updateDoc
i doc
:
import { updateDoc, doc } from "firebase/firestore";
Stwórz funkcję aktualizującą:
const handleUpdateTodo = async (todo) => {
try {
await updateDoc(doc(db, "todos", todo.id), { done: !todo.done });
await fetchTodos();
} catch (err) {
console.error(err);
}
};
Podłącz do onClick
i dodaj stylowanie:
<p
onClick={() => handleUpdateTodo(todo)}
className={`select-none ${todo.done ? "line-through text-zinc-500" : ""}`}
>
{todo.todo}
</p>
Zaimportuj deleteDoc
:
import { deleteDoc } from "firebase/firestore";
Stwórz funkcję handleDeleteTodo
:
const handleDeleteTodo = async (id) => {
try {
await deleteDoc(doc(db, "todos", id));
await fetchTodos();
} catch (err) {
console.error(err);
}
};
Podłącz do onDoubleClick
:
<p
onClick={() => handleUpdateTodo(todo)}
onDoubleClick={() => handleDeleteTodo(todo.id)}
className={`select-none ${todo.done ? "line-through text-zinc-500" : ""}`}
>
{todo.todo}
</p>
Zbudowałeś prostą aplikację TODO z React i Firebase obsługującą dodawanie, odczytywanie, aktualizowanie i usuwanie danych.
import {
addDoc,
collection,
deleteDoc,
doc,
getDocs,
updateDoc,
} from "firebase/firestore";
import { useEffect, useState } from "react";
import { db } from "./lib/firebase";
export default function App() {
const [todo, setTodo] = useState("");
const [todos, setTodos] = useState([]);
const fetchTodos = async () => {
try {
const snapshot = await getDocs(collection(db, "todos"));
const data = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
setTodos(data);
} catch (err) {
console.error(err);
}
};
const handleUpdateTodo = async (todo) => {
try {
await updateDoc(doc(db, "todos", todo.id), { done: !todo.done });
await fetchTodos();
} catch (err) {
console.error(err);
}
};
useEffect(() => {
fetchTodos();
}, []);
const handleAddTodo = async () => {
try {
const newTodo = { todo, done: false };
setTodo("");
await addDoc(collection(db, "todos"), newTodo);
} catch (err) {
console.error(err);
}
};
const handleDeleteTodo = async (id) => {
try {
await deleteDoc(doc(db, "todos", id));
await fetchTodos();
} catch (err) {
console.error(err);
}
};
return (
<div className="h-screen grid place-content-center">
<div className="w-96">
<h1 className="text-4xl font-bold text-center">Zadania do zrobienia</h1>
<div className="flex gap-1.5 my-4">
<input
placeholder="np. Pójdź na siłownie..."
className="outline-none border-2 px-2.5 py-1.5 rounded-md w-full"
value={todo}
onChange={(e) => setTodo(e.target.value)}
/>
<button
onClick={handleAddTodo}
className="bg-black text-white hover:bg-black/80 transition-colors px-2.5 font-medium cursor-pointer rounded-md"
>
Dodaj
</button>
</div>
<div className="font-medium flex flex-col gap-1.5">
{todos.map((todo) => (
<p
key={todo.id}
onClick={() => handleUpdateTodo(todo)}
onDoubleClick={() => handleDeleteTodo(todo.id)}
className={`select-none ${
todo.done ? "line-through text-zinc-500" : ""
}`}
>
{todo.todo}
</p>
))}
</div>
</div>
</div>
);
}