React.js savjeti koji će vas doista učiniti boljim developerom

React.js savjeti koji će vas doista učiniti boljim developerom

React je jednostavan za pokrenuti, ali teško ga je raditi dobro. Većina developera nauči osnove – komponente, props, state – i tu stane, dok stvarna razlika između prosječnog i dobrog React koda leži u manje očiglednim navikama: kako se organizira state, kada se nešto re-renderira i zašto, te kako se piše kod koji ostaje razumljiv i nakon šest mjeseci. U ovom članku prolazimo kroz konkretne, primjenjive savjete koji direktno utječu na kvalitetu, performanse i održivost vaših React aplikacija.


1. Razumijte zašto se komponenta re-renderira

Najveći broj problema s performansama u Reactu dolazi od nepotrebnih re-rendera. Komponenta se re-renderira kada se promijeni njezin state, kada se promijeni state roditelja (pa se proslijede novi props), ili kada se promijeni kontekst koji komponenta koristi.

Prije optimizacije, prvo morate znati što i zašto se re-renderira. React DevTools Profiler je za to nezamjenjiv – omogućuje vam da snimite interakciju i vidite točno koje komponente su se re-renderirale i koliko je to trajalo.

// Često nepotreban re-render: novi objekt na svaki render
<UserCard style={{ padding: 10 }} />

// Bolje: definirajte objekt izvan komponente ili koristite useMemo
const cardStyle = { padding: 10 };
<UserCard style={cardStyle} />

2. Ne pretjerujte s useEffect-om

useEffect je jedan od najčešće zloupotrijebljenih hookova. Mnogi ga koriste za sinkronizaciju state-a koji se mogao izračunati direktno tijekom rendera, što stvara nepotrebne dodatne render cikluse i bugove koji su teški za debug.

// Loše: state koji ovisi o drugom state-u, sinkroniziran kroz effect
const [fullName, setFullName] = useState("");
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// Bolje: derived state se računa direktno
const fullName = `${firstName} ${lastName}`;

useEffect rezervirajte za stvarnu sinkronizaciju s vanjskim sustavima – API pozivi, event listeneri, subscriptions, manipulacija DOM-om izvan Reacta. Ako se nešto može izračunati iz postojećih props-a ili state-a tijekom rendera, nemojte to stavljati u effect.

3. Podignite state samo onoliko visoko koliko treba (state colocation)

Česta greška je staviti sav state na vrh komponentnog stabla “da bude dostupan svima”, što uzrokuje da svaka promjena state-a re-renderira veliki dio aplikacije.

Pravilo je jednostavno: state treba živjeti što bliže komponentama koje ga koriste. Ako samo jedna komponenta koristi neki podatak, taj state pripada njoj, ne roditelju tri razine iznad.

// Loše: state za otvaranje modala živi u root komponenti
function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);
  return (
    <>
      <Header />
      <ProductList />
      <Footer onContactClick={() => setIsModalOpen(true)} />
      {isModalOpen && <ContactModal onClose={() => setIsModalOpen(false)} />}
    </>
  );
}

// Bolje: state je lokaliziran u Footer komponenti koja ga koristi
function Footer() {
  const [isModalOpen, setIsModalOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsModalOpen(true)}>Kontakt</button>
      {isModalOpen && <ContactModal onClose={() => setIsModalOpen(false)} />}
    </>
  );
}

4. Koristite key prop ispravno – nikad index kao zamjenu za ID

key prop pomaže Reactu da prepozna koji se element u listi promijenio, dodao ili uklonio. Korištenje array indexa kao key-a izgleda bezopasno, ali stvara probleme čim se redoslijed liste promijeni – input vrijednosti, animacije i lokalni state komponenti mogu se “zalijepiti” na pogrešne stavke.

// Rizično: index kao key
{items.map((item, index) => (
  <TodoItem key={index} item={item} />
))}

// Sigurno: stabilan, jedinstven identifikator
{items.map((item) => (
  <TodoItem key={item.id} item={item} />
))}

Index kao key je prihvatljiv samo ako je lista statična, nikad se ne sortira, filtrira ili mijenja redoslijed – što je u praksi rijetko.

5. Razbijajte velike komponente prema odgovornosti, ne prema veličini

Komponenta od 400 linija nije nužno problem zbog dužine, nego zbog toga što vjerojatno radi previše stvari – dohvaća podatke, upravlja formom, prikazuje UI i obrađuje logiku validacije, sve u jednoj funkciji.

Korisno pitanje: “Ako objašnjavam što ova komponenta radi, koristim li riječ ‘i’?” Ako da (“dohvaća podatke i prikazuje listu i upravlja filterima”), vjerojatno je vrijeme za podjelu – bilo na manje komponente, bilo izdvajanjem logike u custom hookove.

// Umjesto jedne velike komponente
function ProductsPage() {
  // dohvaćanje podataka, filtriranje, sortiranje, prikaz - sve ovdje
}

// Razdvojite odgovornosti
function ProductsPage() {
  const { products, isLoading } = useProducts();
  const { filters, setFilter } = useProductFilters();
  const visibleProducts = useFilteredProducts(products, filters);

  return (
    <>
      <ProductFilters filters={filters} onChange={setFilter} />
      <ProductList products={visibleProducts} isLoading={isLoading} />
    </>
  );
}

6. Custom hookovi su za logiku, ne samo za “DRY”

Custom hookovi se često koriste samo da se izbjegne ponavljanje koda, ali njihova prava vrijednost je u tome što omogućuju da se UI komponenta fokusira isključivo na prikaz, dok hook nosi logiku – dohvaćanje podataka, pretplate, lokalni state s pravilima.

function useDebouncedValue(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timeout = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timeout);
  }, [value, delay]);

  return debouncedValue;
}

Ovaj hook se može testirati neovisno o UI-u, ponovno koristiti u bilo kojoj komponenti i čini kod komponente koja ga koristi puno čitljivijim.

7. Memoizacija je alat za performanse, ne default pristup

React.memo, useMemo i useCallback nisu nešto što se “uvijek treba koristiti” – svaki od njih ima svoj trošak (memorija, usporedba ovisnosti). Korišteni bez razloga, mogu kod učiniti težim za čitanje bez stvarne koristi.

Memoizacija ima smisla kada:

  • Komponenta radi skupu kalkulaciju koja se ne mora ponavljati na svaki render
  • Komponenta je dio liste s puno stavki i re-renderira se često
  • Funkcija se prosljeđuje kao prop komponenti omotanoj u React.memo, gdje bi nova referenca poništila smisao memoizacije
// Ima smisla: skupa kalkulacija
const sortedData = useMemo(() => expensiveSort(data), [data]);

// Vjerojatno nepotrebno: jednostavna operacija
const fullName = useMemo(() => `${first} ${last}`, [first, last]);

Pravilo: prvo napišite jednostavan kod, zatim mjerite (Profilerom), pa optimizirajte tamo gdje stvarno postoji problem.

8. TypeScript nije opcija – smanjuje broj bugova prije nego se dogode

Tipovi za props, state i API odgovore hvataju cijelu kategoriju grešaka (pristup nepostojećem polju, pogrešan tip podatka, zaboravljeni opcionalni prop) u trenutku pisanja koda, prije nego dođu do produkcije.

type ProductCardProps = {
  product: {
    id: string;
    name: string;
    price: number;
    imageUrl?: string;
  };
  onAddToCart: (productId: string) => void;
};

function ProductCard({ product, onAddToCart }: ProductCardProps) {
  return (
    <div>
      <h3>{product.name}</h3>
      <p>{product.price} €</p>
      <button onClick={() => onAddToCart(product.id)}>Dodaj u košaricu</button>
    </div>
  );
}

Posebno u Next.js projektima, gdje se podaci često dohvaćaju s WordPress REST API-ja ili drugih izvora, jasno tipiziran odgovor sprječava da se greška u strukturi podataka pojavi tek kad korisnik klikne na praznu stranicu.

9. Učitavanje podataka: izbjegavajte “waterfall” zahtjeve

Česta zamka u komponentama koje dohvaćaju podatke u useEffect-u je kreiranje lanca zahtjeva – jedan API poziv čeka drugi, koji čeka treći, premda nemaju stvarnu ovisnost.

// Loše: sekvencijalni zahtjevi koji se mogu paralelizirati
useEffect(() => {
  fetchUser().then((user) => {
    fetchOrders(user.id).then((orders) => {
      fetchRecommendations(user.id).then(setRecommendations);
    });
  });
}, []);

// Bolje: paralelni zahtjevi gdje je to moguće
useEffect(() => {
  fetchUser().then((user) => {
    Promise.all([
      fetchOrders(user.id),
      fetchRecommendations(user.id),
    ]).then(([orders, recommendations]) => {
      setOrders(orders);
      setRecommendations(recommendations);
    });
  });
}, []);

U Next.js App Routeru, dio ovih problema se rješava na razini servera kroz paralelno dohvaćanje podataka u Server Componentima, što dodatno smanjuje vrijeme do prvog prikaza sadržaja.

10. Pišite testove za logiku, ne za implementaciju

Testovi koji provjeravaju interne detalje implementacije (npr. “je li state varijabla X jednaka Y”) puknu na svaku refaktorizaciju, čak i ako se ponašanje aplikacije nije promijenilo. Korisniji su testovi koji provjeravaju ponašanje iz perspektive korisnika.

// Manje korisno: testira implementaciju
expect(component.state.isOpen).toBe(true);

// Korisnije: testira ponašanje
fireEvent.click(screen.getByText("Otvori meni"));
expect(screen.getByRole("menu")).toBeVisible();

Ovakav pristup (npr. uz React Testing Library) znači da testovi i dalje prolaze čak i ako promijenite interni state management, dok god krajnje ponašanje ostaje isto.

Zaključak

Bolji React developer ne znači developer koji znade više hookova ili sintaktičkih trikova – znači developer koji razumije zašto se nešto događa (re-render, re-fetch, re-render lanca), gdje treba živjeti odgovornost (state colocation, custom hookovi) i kada optimizacija stvarno donosi vrijednost. Ovi principi posebno dolaze do izražaja u produkcijskim Next.js projektima, gdje performanse, SEO i održivost koda direktno utječu na poslovni rezultat.

FAQ

Treba li svaki projekt koristiti TypeScript? Za projekte koji rastu, imaju više developera ili se dugoročno održavaju, TypeScript se isplati od prvog dana – troškovi pisanja tipova su mnogo manji od troška debugiranja grešaka u produkciji.

Je li useEffect loš? Ne, ali se često koristi za stvari koje useEffect ne treba rješavati. Pravilo je: koristite ga za sinkronizaciju s vanjskim sustavima (API, DOM, subscriptions), a ne za izračunavanje vrijednosti iz postojećeg state-a.

Kada je vrijeme za podjelu velike komponente? Kada komponenta radi više od jedne stvari – dohvaća podatke, upravlja formom i prikazuje kompleksan UI istovremeno. Podjela prema odgovornosti olakšava testiranje, čitanje i ponovnu upotrebu.

Koliko memoizacija stvarno utječe na performanse? Ovisi o veličini aplikacije i broju re-rendera. Najbolji pristup je profilirati prije i poslije – u manjim aplikacijama razlika je često neprimjetna, dok u velikim listama i kompleksnim formama može biti značajna.


Ako razvijate Next.js aplikaciju ili dashboard i želite da kod bude brz, čitljiv i lako proširiv, MasteryMesh može pomoći s razvojem ili tehničkim auditom postojećeg projekta. Kontaktirajte nas ili pogledajte našu uslugu Web aplikacije.

Povezani članci