CRUD operacije

CRUD operacije

Do sada smo se upoznali samo sa GET zahtjevom koji nam služi za dohvat podataka. Sada ćemo kroz primjer jednostavne aplikacije proći i kroz ostale vrste zahtjeva koje nam omogućavaju osnovne CRUD operacije.

CRUD označava osnovne operacije nad podacima:

  • CreatePOST
  • ReadGET
  • UpdatePUT / PATCH
  • DeleteDELETE

JSONPlaceholder (opens in a new tab) API, kojeg smo već prethodno koristili, nudi testne podatke poput posts, comments, albums i sl. Omogućuje svaku od ovih operacija i vraća realistične odgovore, kakve bi dobili od "pravog" poslužitelja (podaci se ne mijenjaju trajno, samo se simulira izvršavanje tih akcija).

Dohvat podataka

S obzirom da smo prošli već dva primjera dohvata podataka, sada ćemo samo napraviti početni izgled naše React aplikacije te dohvatiti i prikazati popis postova (engl. posts):

App.jsx
import { useState, useEffect } from "react";
import axios from "axios";
 
function App() {
  const [posts, setPosts] = useState([]);
 
  useEffect(() => {
    axios
      .get("https://jsonplaceholder.typicode.com/posts")
      .then(response => setPosts(response.data))
      .catch(err => console.log(err));
  }, []);
 
  return (
    <div>
      <h1>Popis postova</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            #{post.id} {post.title}
          </li>
        ))}
      </ul>
    </div>
  );
}
 
export default App;

Slanje (POST) podataka

Kako bismo omogućili dodavanje (kreiranje) novih postova, iskoristit ćemo POST zahtjev prema JSONPlaceholder API-ju. Za bolju preglednost kôda, izdvojit ćemo formu za unos u zasebnu React komponentu.


Stvaranje forme za unos

U posebnoj datoteci, npr. NoviPostForma.jsx, napravit ćemo komponentu koja sadrži formu za unos novog posta. Njezina zadaća je prikupiti title i body te, nakon pritiska tipke Spremi, pozvati funkciju (koju joj prosljeđuje roditeljska komponenta) koja će odraditi slanje POST zahtjeva na poslužitelj.

NoviPost.jsx
import { useState } from "react";
 
function NoviPostForma({ onSubmit }) {
  const [noviPost, postaviNoviPost] = useState({ title: "", body: "" });
 
  const promjenaUnosa = (e) => {
    postaviNoviPost({ ...noviPost, [e.target.name]: e.target.value });
  };
 
  const handleSubmit = (e) => {
    e.preventDefault();
    // Pozivamo funkciju iz props-a koja šalje POST zahtjev
    onSubmit(noviPost);
    // Resetiramo lokalno stanje forme
    postaviNoviPost({ title: "", body: "" });
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>
          Naslov:
          <input
            type="text"
            name="title"
            value={noviPost.title}
            onChange={promjenaUnosa}
            required
          />
        </label>
      </div>
 
      <div>
        <label>
          Opis:
          <textarea
            name="body"
            value={noviPost.body}
            onChange={promjenaUnosa}
            required
          />
        </label>
      </div>
 
      <button type="submit">Spremi</button>
    </form>
  );
}
 
export default NoviPostForma;
💡

Napomena: Ova komponenta prima prop onSubmit – to je funkcija pomoću koje ćemo iz roditeljske komponente proslijediti logiku za slanje POST zahtjeva. Ovaj pristup održava dobru razdvojenost odgovornosti (forma skuplja podatke, a roditelj odlučuje što s njima)."

2.2. Slanje POST zahtjeva

U glavnoj komponenti, iskoristiti ćemo Axios za slanje POST zahtjeva na https://jsonplaceholder.typicode.com/posts (opens in a new tab). Nakon uspješnog odgovora, možemo ažurirati lokalno stanje i time prikazati novi post.

App.jsx
import axios from "axios";
import { useState, useEffect } from "react";
import NoviPostForma from "./NoviPostForma";
 
function App() {
  const [posts, setPosts] = useState([]);
 
  useEffect(() => {
    axios
      .get("https://jsonplaceholder.typicode.com/posts")
      .then(response => setPosts(response.data))
      .catch(err => console.log("Greška pri dohvaćanju:", err));
  }, []);
 
  // Funkcija za slanje POST zahtjeva (poslati ćemo je formi)
  const dodajNovi = (noviPost) => {
    axios.post("https://jsonplaceholder.typicode.com/posts", noviPost)
      .then(response => {
        console.log("Odgovor na POST:", response.data);
 
        // Dodajemo novi post u lokalno stanje (za prikaz u listi)
        setPosts(prethodniPostovi => [...prethodniPostovi, response.data]);
      })
      .catch(err => console.log("Greška pri slanju:", err));
  };
 
  return (
    <div>
      <h1>Popis postova</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            #{post.id} {post.title}
          </li>
        ))}
      </ul>
 
      <h2>Dodaj novi post</h2>
      {/* Formu za unos novog posta izdvojili smo u zasebnu komponentu */}
      <NoviPostForma onSubmit={dodajNovi} />
    </div>
  );
}
 
export default App;
💡

S obzirom da se podaci ne spremaju trajno, svaki put kad pošaljemo POST zahtjev, taj podatak će biti spremljen pod ID-om br "101". To će uzrokovati grešku u našoj aplikaciji ako pokušamo poslati više uzastopnih POST zahtjeva, ali sa pravim poslužiteljem to bi ispravno radilo.

U ovom primjeru "App" komponenta brine se o upravljanju stanjem (popis svih postova) i dohvatu podataka (GET).

"NoviPostForma" komponenta upravlja isključivo unosom i validacijom unosa u formi, a za slanje podataka koristi prop (onSubmit) i logiku koja je definirana u roditeljskoj komponenti. Na ovaj način se komponenta može iskoristiti za slanje različitih zahtjeva - unutar komponente imamo samo logiku i stanja koja se direktno tiču te komponente.

Kada dobijemo uspješan odgovor od API-ja (unos novog posta), pristupamo response.data objektu koje sadrži generirani id i sve ostale podatke koju su spremljeni na poslužitelju (poslužitelj nekada može modificirati podatke koje smo mu poslali). Zatim osvježavamo stanje (setPosts) kako bi se odmah prikazao i naš upravo dodani post.

Brisanje (DELETE) podataka

Za početak ćemo modificirati dio glavne aplikacije u kojoj prikazujemo sve poruke. Uz svaku pojedinačnu poruku ćemo dodati button element koji će pozivati funkciju za brisanje tog podataka. ID podatka nam je svakako dostupan prilikom ispisa, pa ga koristimo kao parametar koji šaljemo funkciji za brisanje - ID će nam biti potreban kako bi poslužitelju mogli navesti koji točno podatak želimo izbrisati.

App.jsx
<ul>
  {posts.map(post => (
    <li key={post.id}>
      #{post.id} {post.title} 
      <button onClick={() => obrisiPost(post.id)}>Obriši</button>
    </li>
  ))}
</ul>

Naravno, moramo unutar same komponente i definirati funkciju za brisanje. Znamo da prima "id" parametar i da moramo poslati DELETE zahtjev na određeni URL. Slanje DELETE zahtjeva je prilično jednostavno, pogotovo kada koristimo Axios. Sve što je potrebno je pozvati ugrađenu axios.delete() metodu i upisati ispravnu adresu podatka (resursa) kojeg želimo izbrisati.

Za točnu adresu bi naravno trebalo pogledati dokumentaciju poslužitelja. U našem slučaju lokalni poslužitelj koristi REST arhitekturu što znači da svaki resurs ima jedinstvenu putanju. U našem slučaju to znači da zahtjev za brisanje moramo poslati na adresu posts/id, gdje umjesto id šaljemo brojčanu vrijednost koja odgovara ID podatku kojeg želimo izbrisati. Implementacija funkcije izgleda ovako:

App.jsx
const obrisiPost = (id) => {
  axios.delete(`https://jsonplaceholder.typicode.com/posts/${id}`)
    .then((res) => {
      console.log(`Post ${id} obrisan (simulirano)!`);
      console.log(res)
 
      // Uklonimo post iz lokalnog stanja
      setPosts(prethodniPostovi => prethodniPostovi.filter(p => p.id !== id));
    })
    .catch(err => console.log(err));
};

Još jednom, API koji koristimo neće zapravo izbrisati podatke ali će simulirati proces brisanja i poslati odgovarajući odgovor. Većina poslužitelja će kao odgovor na uspješnu akciju brisanja vratiti status 200 (OK), 204 (No content) ili 202 (Accepted). U našem slučaju API vraća status 200, bez podataka. Neki poslužitelji također znaju vratiti aktualnu kolekciju podataka (bez upravo obrisanog). Ovaj API to ne radi pa koristimo ugrađenu filter funkciju kako bi modificirali postojeće stanje i izbacili podatak sa ID-em kojeg smo upravo izbrisali na poslužitelju.

Ažuriranje (PUT/PATCH) podataka

Preostale su nam još akcije za ažuriranje podataka. HTTP protokol podržava dvije metode za osvježavanje podataka - PUT i PATCH. Ove metode su na prvi pogled identične ali postoji bitna razlika pa ćemo se kratko osvrnuti na obje metode.

PUT metoda

PUT metoda služi za osvježavanje već postojećeg resursa na poslužitelju. Glavna karakteristika ove metode je u tome što zamjenjuje cijeli resurs sa podatkom kojeg smo poslali u tijelu zahtjeva. Čak i ako želimo promijeniti samo dio podatka, u tijelu zahtjeva moramo poslati potpuni podatak.

Na primjer ako imamo strukturu podatka sa tri polja: "ime", "mail" i "godine", PUT zahtjev mora u svom tijelu (body) sadržavati podataka u obliku:

{
  "ime": "Ana Anić",
  "mail": "anaanic@mail.com",
  "godine": 24
}

Zahtjev šaljemo na adresu već postojećeg resursa, najčešće po ID-u. Na primjer u gornjem slučaju bi adresi bila u obliku "/osobe/1" i umjesto postojećih podataka za osobu sa ID: 1 bi se spremili gore navedeni podaci. Čak i kada bi npr. htjeli samo promijeniti godine, PUT zahtjev mora sadržavati i ostale podatke kako bi ostali spremljeni na poslužitelju.

PATCH metoda

PATCH metoda se također koristi za modifikaciju postojećih resursa na poslužitelju ali umjesto zamjene cijelog podatka osvježava se samo dio resursa, onaj koji se nalazi u tijelu samog zahtjeva.

Korištenjem ove metode možemo raditi parcijalnu zamjenu podataka što može doprinijeti boljoj efikasnosti aplikacije i smanjenom mrežnom prometu. Uzmemo li u obzir prethodni primjer i situaciju kada želimo samo promijeniti godine, dovoljno bi bilo uz PATCH zahtjev poslati podatak u obliku:

{
  "godine": 25
}

U ovom slučaju će se odabranom podatku (pretpostavimo da je adresa ponovno "/osobe/1") promijeniti samo svojstvo "godine" na novu vrijednost, dok će sve ostale vrijednosti ostati nepromijenjene.

Iz ovih primjera je očito i kada izabrati koju metodu. Ukoliko želite zamijeniti cijeli podatak na poslužitelju, koristite PUT metodu. S druge strane, djelomične promjene podataka je bolje raditi uz pomoć PATCH metode. Naravno, to sve ovisi i o implementaciji poslužitelja i koje metode podržava.

Axios primjeri

Pogledajmo sada jednostavni primjer kako koristiti ove metode pomoću Axios biblioteke na našoj trenutnoj aplikaciji. Krenimo od PUT metode koja mijenja cijeli resurs. Napisati ćemo funkciju za zamjenu resursa. Axios ima ugrađenu axios.put() metodu kojoj moramo uz URL resursa, poslati i tijelo zahtjeva koje će sadržavati (cijeli) novi resurs. U našem slučaju moramo poslati "title" i "body" vrijednosti:

App.jsx
const osvjeziPost = (id) => {
  const azuriraniPodatak = { title: "Novi naslov", body: "Ažurirani sadržaj" };
 
  axios.put(`https://jsonplaceholder.typicode.com/posts/${id}`, azuriraniPodatak)
    .then(response => {
      console.log("Odgovor na PUT:", response.data);
 
      // Ažuriraj i u lokalnom stanju
      setPosts(prethodni =>
        prethodni.map(p =>
          p.id === id ? response.data : p
        )
      );
    })
    .catch(err => console.log(err));
};

Ovdje je ponovno potrebno napomenuti da je ažuriranje samo simulirano. Poslužitelj kao odgovor vraća ažurirani podatak (uvijek postoji mogućnost da je dodatno modificiran od strane poslužitelja). Jedini složeni dio gornje funkcije je logika osvježavanja lokalnog stanja. Sa map metodom prolazimo po trenutnom stanju postova i tražimo koji nam se post podudara po ID parametru. Kada pronađemo taj post zamijeniti ćemo ga sa odgovorom sa poslužitelja, dok će svi ostali postovi ostati identični prethodnom stanju.

Trenutno nigdje ne pozivamo ovu funkciju, možete samostalno dodati novi button element kao što imamo već za brisanje ili taj isti možemo modificirati kako bi mijenjao podatke umjesto brisanja. Naravno, bilo bi dobro omogućiti korisniku na sučelju da sam unosi novi naslov i sadržaj ali to možete pokušati samostalno implementirati, uz pomoć uvjetnog renderiranja. Za sada je samo bitno demonstrirati rad DELETE metode.

Za kraj možemo vidjeti i kako bi izgledala implementacija PATCH metode tj. funkcije koja bi radila samo djelomično ažuriranje podataka. Ponovno se koristi ugrađena axios metoda axios.patch() kojoj šaljemo URL i tijelo. Jedina razlika u odnosu na PUT je što tijelo ne mora sadržavati cijeli podatak.

App.jsx
const djelomicnoOsvjeziPost = (id) => {
  axios.patch(`https://jsonplaceholder.typicode.com/posts/${id}`, { title: "Samo novi naslov" })
    .then(response => {
      console.log("Odgovor na PATCH:", response.data);
 
      // Ažuriraj lokalno stanje
      setPosts(prethodni =>
        prethodni.map(p =>
          p.id === id ? { ...p, title: "Samo novi naslov" } : p
        )
      );
    })
    .catch(err => console.log(err));
};

Možemo uočiti da objekt kojeg šaljemo kao tijelo zahtjeva sadrži samo "title" svojstvo. Rezultat koji dobijemo od poslužitelja je ažurirani podatak koji je zadržao staru vrijednost "body" svojstva i postavio novu vrijednost naslova. Logika ažuriranja lokalnog stanja je slična, samo ovaj put također radimo djelomično ažuriranje postojećeg objekta. U ovom slučaju nam poslužitelj šalje cijeli podatak kao odgovor pa smo mogli i njega iskoristiti, implementacija bi također bila ispravna.

Digitalna Dalmacija