Poznajemy podstawy tworzenia aplikacji Reactowych oraz komponentów funkcyjnych. Dowiadujemy się czym są props oraz jak używać podstawowych hooków takich jak useState, useEffect oraz useRef.

Tworzymy projekt React – pierwszy rzut okiem

Zakładam, że wszystko potrzebne do pracy mamy zainstalowane, ewentualnie potrafimy zainstalować podstawowe narzędzia do pracy z biblioteką React. Naszą pierwszą aplikację React w łatwy sposób utworzymy przy pomocy komendy:

npx create-react-app first-app

Będziemy musieli poczekać, aż wszystko zostanie poprawnie utworzone i zainstalowane – zostaniemy o tym poinformowani w konsoli. Wtedy przejdziemy do folderu aplikacji przy użyciu komendy cd (change directory):

cd first-app

Następnie uruchomimy naszą aplikację przy pomocy komendy:

npm start

Nasza aplikacja powinna otworzyć się w domyślnej dla naszego systemu przeglądarce internetowej, jeżeli z jakichś powodów się nie otwarła to znajduje się ona pod adresem:

http://localhost:3000/

Zauważmy, że ta komenda cały czas „chodzi” w terminalu. Jeżeli chcemy przestać serwować naszą aplikację musimy wcisnąć „CTRL+C” w terminalu i potwierdzić zamknięcie poprzez wpisanie „y” oraz wciśnięcie ENTER. Wtedy aplikacja przestanie być serwowana.

Nasza aplikacja znajduje się w folderze src w pliku App.js:

import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

Możemy tutaj zmienić to i owo według własnych upodobań. Warto zwrócić uwagę na kilka szczegółów. Po pierwsze – to nie HTML ale JSX w pliku JavaScript. I choć markup może wyglądać podobnie – zauważmy, że elementy JSX nie mogą mieć nazw takich jak słówka kluczowe języka JavaScript.

W HTMLu klasy elementów obsługuje atrybut o nazwie „class”. Nazwie takiej samej jak słówko kluczowe „class” języka JavaScript. W Reactowym JSX nie ma na to miejsca – dlatego używamy atrybutu className:

 <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>

Warto też zwrócić uwagę na to, w jaki sposób wartości atrybutów są dynamicznie przekazywane poprzez nawiasy klamrowe {}:

<img src={logo} className="App-logo" alt="logo" />

Wreszcie, rzecz niezwykle istotna – w React możemy zwracać tylko jeden element nadrzędny. Taki kod spowoduje błąd:

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
    <p>Hello!</p>
  );
}

Mamy tutaj dwa elementy nadrzędne. Oczywiście jest na to proste rozwiązanie – opleciemy nasze elementy pustym tagiem <> oraz </> oznaczającym Reactowy fragment, czyli coś stworzonego właśnie na taką okazję:

import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <>
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
    <p>Hello!</p>
    </>
  );

}

export default App;

Warto jeszcze pochylić się nad stylowaniem elementów – możemy to robić w pliku App.css, który importujemy. Możemy też używać atrybutu style, do którego przekazujemy obiekt, zawierający klucze i wartości:

<p style={{color: 'black', backgroundColor: 'tomato'}}>Hello!</p>

To nie CSS – nie ma tu średników, są przecinki, jak w obiekcie. Mało tego – nazwy takie jak 'background-color’ to tutaj 'backgroundColor’. Jeszcze ten podwójny nawias wąsaty – jeden to interpolacja wartości do atrybutu, drugi to literał obiektu. Możemy to nieco uprościć:

import logo from './logo.svg';
import './App.css';

function App() {
  let pStyle = {color: 'black', backgroundColor: 'tomato', fontWeight: 'bold'};
  return (
    <>
    <div className="App">
      (...)
    </div>
    <p style={pStyle}>Hello!</p>
    </>
  );

}

export default App;

W ten sposób stylujemy elementy Reacta.

Hook useState – piszemy prosty licznik

Napiszemy prosty licznik wykorzystując jeden z najbardziej użytecznych hooków Reacta – useState. Na początku zaimportujmy go i przyjrzyjmy się mu:

import React, { useState } from 'react';

function App() {
  const [count, setCount] = useState(5);

  return (
    <>
      <h1>Counter: {count}</h1>
    </>
  );

}

export default App;

Hook useState przyjmuje wartość początkową – tutaj 5 – i zwraca dwie rzeczy. Po pierwsze nasz stan (pod nazwą count), po drugie – funkcję służącą do zmiany stanu (pod nazwą setCount). Stan już sobie podpięliśmy do naszego <h1> i powinien być widoczny. Teraz możemy Zabawić się z funkcją setCount:

import React, { useState } from 'react';

function App() {
  const [count, setCount] = useState(5);


  
  return (
    <>
      <h1>Counter: {count}</h1>
      <button onClick={() => setCount(6)}>Zwiększ</button>
      <button onClick={() => setCount(4)}>Zmniejsz</button>
    </>
  );

}

export default App;

Może i dziwnie się te funkcje do zdarzenia onClick w React przekazuje – ale tak to wygląda. Spróbujmy sobie już poprawnie napisać te funkcje „na zewnątrz”:

import React, { useState } from 'react';

function App() {
  const [count, setCount] = useState(5);
  const increment = () => {
            setCount(count + 1);
        };
    
      const decrement = () => {
            setCount(count - 1);
        };

  
  return (
    <>
      <h1>Counter: {count}</h1>
      <button onClick={increment}>Zwiększ</button>
      <button onClick={decrement}>Zmniejsz</button>
    </>
  );

}

export default App;

Możemy teraz dowolnie zwiększać/zmniejszać wartość count naszego countera – i wszystko poprawnie działa i się wyświetla. A gdyby tak dodać guzik „reset”? Nie powinniśmy już teraz mieć z tym problemu:

import React, { useState } from 'react';

function App() {
  const [count, setCount] = useState(5);
  const increment = () => {
      setCount(count + 1);
  };
    
  const decrement = () => {
      setCount(count - 1);
  };
  const reset = () => {
      setCount(0);
  };
    
  return (
    <>
      <h1>Counter: {count}</h1>
      <button onClick={increment}>Zwiększ</button>
      <button onClick={reset}>Resetuj</button>
      <button onClick={decrement}>Zmniejsz</button>
    </>
  );

}

export default App;

Counter jako osobny komponent – podstawy komponentów

W folderze src utworzymy nowy folder o nazwie components, w którym utworzymy plik „Counter.js” o poniższej zawartości:

import React, { useState } from 'react';

function Counter() {
    const [count, setCount] = useState(5);

    const increment = () => {
        setCount(count + 1);
    };

    const decrement = () => {
        setCount(count - 1);
    };
    const reset = () => {
        setCount(0);
    };
    return (
        <div>
            <h1>Counter: {count}</h1>
            <button onClick={increment}>Zwiększ</button>
             <button onClick={reset}>Resetuj</button>
             <button onClick={decrement}>Zmniejsz</button>
        </div>
    );
}

export default Counter;

To cały czas nasz Counter z poprzedniego przykładu, ale teraz używamy go jako komponentu. Teraz pora zaimportować i użyć go w pliku App.js:

import React from 'react';
import Counter from './components/Counter';

function App() {
  return (
    <>
      <Counter/>
    </>
  );
}

export default App;

Już. To takie proste. Oczywiście, możemy wykorzystać ten komponent wielokrotnie:

import React from 'react';
import Counter from './components/Counter';

function App() {
  return (
    <>
      <Counter/>
      <Counter/>
    </>
  );

}

export default App;

Każdy ma swój odrębny stan, jest odrębnym bytem – zwiększanie/zmniejszanie/restartowanie jednego nie wpływa na drugi. Mało tego – możemy wykorzystać coś takiego jak props. Jak to działa?

W pliku Counter.js dodajmy co następuje:

import React, { useState } from 'react';

function Counter(props) {
    const initialCount = props.initial ?? 5;
    const [count, setCount] = useState(initialCount);

    const increment = () => {
        setCount(count + 1);
    };

    const decrement = () => {
        setCount(count - 1);
    };
    const reset = () => {
        setCount(0);
    };
    return (
        <div>
            <h1>Counter: {count}</h1>
            <button onClick={increment}>Zwiększ</button>
             <button onClick={reset}>Resetuj</button>
             <button onClick={decrement}>Zmniejsz</button>
        </div>
    );
}

W ten sposób jesteśmy w stanie podać wartość początkową o nazwie initial dla naszego countera. Jeżeli jest pusta – użyjemy liczby 5. Jeżeli nie – użyjemy tego, co przekażemy jako atrybut do komponentu <Counter/>. Jak sprawdzić czy to działa, najlepiej w obu przypadkach?

import React from 'react';
import Counter from './components/Counter';

function App() {
  return (
    <>
      <Counter initial={3}/>
      <Counter/>
    </>
  );
}

export default App;

Pierwszy licznik ma wartość początkową ustawioną na 3. Drugi – na 5. Możemy jeszcze zaimplementować mechanizm, który sprawia, że „reset” wraca do wartości początkowej:

const reset = () => {
        setCount(initialCount);
    };

Warto jeszcze nadmienić o dekompozycji propsów, która może nam ułatwić nieco życie. Wygląda to tak:


function Counter({initial}) {
    const initialCount = initial ?? 5;
(...)

Hook useEffect – logowanie zmian

Przyjrzyjmy się raz jeszcze naszemu komponentowi Counter:

import React, { useState } from 'react';

function Counter({initial}) {
    const initialCount = initial ?? 5;
    const [count, setCount] = useState(initialCount);

    const increment = () => {
        setCount(count + 1);
    };

    const decrement = () => {
        setCount(count - 1);
    };
    const reset = () => {
        setCount(initialCount);
    };
    return (
        <div>
            <h1>Counter: {count}</h1>
            <button onClick={increment}>Zwiększ</button>
             <button onClick={reset}>Resetuj</button>
             <button onClick={decrement}>Zmniejsz</button>
        </div>
    );
}

export default Counter;

Może najść nas ochota, aby przy każdej zmianie stanu naszego 'count’ nowa wartość była logowana w konsoli. Wydawać by się mogło, że nic łatwiejszego:

const increment = () => {
        setCount(count + 1);
        console.log(count);
    };

    const decrement = () => {
        setCount(count - 1);
        console.log(count);
    };
    const reset = () => {
        setCount(initialCount);
        console.log(count);
    };

Takie rozwiązanie jest złe – nie tylko ze względu na niepotrzebną repetytywność. Po prostu – źle funkcjonuje z Reactem, co możemy zaobserwować. React jest asynchroniczny, React specyficznie działa 'pod spodem’ i wreszcie – React posiada własne narzędzia do obsługi takich sytuacji, jak wykonanie jakiegoś zadania podczas zmiany stanu.

Takim narzędziem jest hook useEffect, który wykonuje jakiś efekt (jakąś funkcję) ilekroć stan/stany podane do nasłuchiwania się zmienią. Poprawne użycie tego hooka w naszym komponencie wygląda tak:

import React, { useState, useEffect } from 'react';

function Counter({initial}) {
    const initialCount = initial ?? 5;
    const [count, setCount] = useState(initialCount);
    useEffect( () => {
        console.log(count);
    }, [count]);

    const increment = () => {
        setCount(count + 1);
    };

    const decrement = () => {
        setCount(count - 1); 
    };
    const reset = () => {
        setCount(initialCount);
    };
    return (
        <div>
            <h1>Counter: {count}</h1>
            <button onClick={increment}>Zwiększ</button>
             <button onClick={reset}>Resetuj</button>
             <button onClick={decrement}>Zmniejsz</button>
        </div>
    );
}

export default Counter;

Zwróćmy uwagę, jak ten hook jest skonstruowany:

useEffect( () => {
        console.log(count);
    }, [count]);

Pierwsze, co do niego przekazujemy, to funkcja, która ma się wykonać przy zmianie stanu. Drugie, to tablica zależności – zmiany jakiego stanu mają tę funkcję / ten efekt wywoływać.

A co gdybyśmy chcieli dodać efekt, który wykona się jeden raz, po utworzeniu komponentu? Wtedy musimy podać pustą tablicę zależności:

useEffect( () => {
        console.log(`Komponent Counter dodany do strony.`);
    }, []);

Taki kod wykona się jeden raz, na samym początku, podczas gdy poprzedni efekt – zarówno na początku jak i przy każdej zmianie stanu count. Możemy też nie podać żadnej tablicy zależności w ogóle – wtedy funkcja useEffect będzie wykonywana przy dodaniu elementu albo zmianie jakiegokolwiek stanu.

Hook useEffect – interwał i funkcja sprzątająca

Utworzymy sobie nowy komponent o nazwie IntervalCounter w pliku IntervalCounter.js (w folderze Components, wewnątrz src naszego projektu). Na razie będzie wyglądał w ten sposób:

import React, { useState, useEffect } from 'react';

function IntervalCounter() {
    
    const [count, setCount] = useState(0);
    
    useEffect( () => {
        console.log(count);
    }, [count]);

    return (
        <div>
            <h1>Counter: {count}</h1>
        </div>
    );
}

export default IntervalCounter;

Możemy teraz dodać go do naszego projektu w App.js:

import React from 'react';
import Counter from './components/Counter';
import IntervalCounter from './components/IntervalCounter';

function App() {
  return (
    <>
      <Counter initial={3}/>
      <Counter/>
      <IntervalCounter/>
    </>
  );
}

export default App;

Na razie nasz IntervalCounter niewiele robi, ale chcemy dodać do niego opcję zwiększania się co sekundę.

import React, { useState, useEffect } from 'react';

function IntervalCounter() {
    
    const [count, setCount] = useState(0);
    
    useEffect( () => {
        const interval = setInterval(() => {
            setCount(count + 1);
        }, 1000);
    }, [count]);

    return (
        <div>
            <h1>Counter: {count}</h1>
        </div>
    );
}

export default IntervalCounter;

Być może wszystko działa nam jak należy – ale funkcja przekazana do useEffect powinna, poza wykonywaniem pewnych czynności, zwracać tzw. cleanup function czyli funkcję czyszczącą. W naszym przypadku będzie to wyczyszczenie interwału:

useEffect( () => {
        const interval = setInterval(() => {
            setCount(count + 1);
        }, 1000);
        return () => clearInterval(interval);
    }, [count]);

Jest to dobra praktyka, której powinniśmy przestrzegać. Używamy interwału – zwracamy funkcję czyszczącą interwał. Inne przykłady funkcji sprzątających to zamykanie WebSockets:

useEffect(() => {
    const socket = new WebSocket('wss://example.com');
    // ... logika obsługi połączenia ...

    return () => {
        socket.close();
    };
}, []);

Podobnie, w useEffect sprzątamy wszystkie dodawane w obrębie useEffect event-listenery:

useEffect(() => {
    const handleClick = () => {
        // ... obsługa kliknięcia ...
    };

    document.addEventListener('click', handleClick);

    return () => {
        document.removeEventListener('click', handleClick);
    };
}, []);

Możemy też wykorzystać funkcję sprzątającą do zwalniania zasobów – jeżeli będziemy o tym pamiętać, możemy oszczędzić pamięć, gdy już nie jest potrzebna:

useEffect(() => {
    const image = new Image();
    image.src = 'https://example.com/image.jpg';

    return () => {
        // Zwalnianie zasobów (np. pamięci podręcznej obrazu)
        image.src = '';
    };
}, []);

Nie jest to coś koniecznego, aczkolwiek warto o tym pamiętać. Natomiast jeżeli chcemy logować w konsoli stan, który się zmienił, wykonywać funkcję podczas zmiany stanu (jakiegokolwiek bądź określonego w tablicy zależności) – MUSIMY używać useEffect. Czy musimy i czy chcemy używać funkcji sprzątającej – to już nasza sprawa.

Zdaję sobie sprawę, że ten Reactowy sposób podchodzenia do różnych zagadnień może wyglądać na początku dziwnie, albo wręcz odstraszająco. Na to nie ma żadnej dobrej recepty – trzeba się po prostu z Reactem, jego składnią, jego specyfiką i dziwactwami obyć, najlepiej pisząc kolejne komponenty i projekty.

Hook useRef – Reactowy query selector

W bibliotece React nie korzystamy z query selectora ani innych tzw. „natywnych” rozwiązań, które sprawdzają się w standardowym DOM i JavaScript. Zamiast tego, do „łapania” elementów w naszym komponencie JSX mamy kolejny hook, jakim jest useRef.

Utworzymy kolejny komponent o nazwie FakeForm w pliku FakeForm.js w folderze Components wewnątrz src naszego projektu. Wrzucimy tam następującą zawartość:

import React, { useRef } from 'react';

function FakeForm() {
    

    const inputElement = useRef();
    
    return (
        <div>
            <input type="text" ref={inputElement}></input>
            <button>Log in console</button>
        </div>
    );
}

export default FakeForm;

Jak widać tworzymy formularz, po kliknięciu którego dane z formularza zostaną wylogowane w konsoli. To napiszmy sobie nasz kod:

import React, { useRef } from 'react';

function FakeForm() {
    

    const inputElement = useRef();
    function onClickHandler() {
        console.log(inputElement.current.value);
    }

    return (
        <div>
            <input type="text" ref={inputElement}></input>
            <button onClick={() => onClickHandler()}>Log in console</button>
        </div>
    );
}

export default FakeForm;

Mamy tutaj referencję do elementu input. Element posiada atrybut current, zaś current posiada value, pod którym skrywa się wartość wpisana do naszego inputu. Ja też tego nie znam na pamięć – początkowo próbowałem inputElement.value, jako że nic nie dało – wylogowałem sobie inputElement w konsoli i sprawdziłem, gdzie się ta nasza wartość kryje.

Oczywiście musimy dodać to do głównego pliku App.js naszego projektu:

import React from 'react';
import Counter from './components/Counter';
import IntervalCounter from './components/IntervalCounter';
import FakeForm from './components/FakeForm';

function App() {
  return (
    <>
      <Counter initial={3}/>
      <Counter/>
      <IntervalCounter/>
      <FakeForm/>
    </>
  );
}

export default App;

Teraz jeszcze możemy zabawić się nie tylko w logowanie ale i usuwanie tekstu wpisanego do input po wciśnięciu przycisku:

import React, { useRef } from 'react';

function FakeForm() {
    

    const inputElement = useRef();
    function onClickHandler() {
        console.log(inputElement.current.value);
        inputElement.current.value = '';
    }

    return (
        <div>
            <input type="text" ref={inputElement}></input>
            <button onClick={() => onClickHandler()}>Log in console</button>
        </div>
    );
}

export default FakeForm;

I tak wygląda useRef – pozwala nam „złapać” element w naszym komponencie, stworzyć referencję do niego, dzięki której można się do niego i jego właściwości odnosić w sposób podobny jak w klasycznym JavaScript łapanie elementów poprzez query selector, zapisywanie do zmiennych i robienie z nimi różnych operacji. W React jest trochę inaczej, ale jednak podobnie.

Renderowanie list komponentów – użycie map

Kolejna umiejętność, jaką warto nabyć to renderowanie wielu komponentów w oparciu o jakiś zbiór. Zrobimy sobie komponent o nazwie ShopItem, w pliku ShopItem.js w folderze components w src naszego projektu. To bardzo prosty komponent:

function ShopItem({name, price}) {
    return <li><strong>Name:</strong> {name} <strong>Price:</strong> {price}</li>
}

export default ShopItem;

Teraz spróbujemy go użyć w naszym App.js:

import React from 'react';
import ShopItem from './components/ShopItem';

function App() {
  return (
    <>
      <ul>
          <ShopItem name={"Chair"} price={"15.00 PLN"} />
      </ul>
    </>
  );
}

export default App;

Jak widać – działa. Dobrze, a teraz zróbmy symulację zaciągnięcia z bazy danych/zewnętrznego API jakichś rekordów w postaci listy obiektów zawierających atrybuty 'name’ oraz 'price’:

const items = [
  {name: "Chair", price: "15 PLN"}, 
  {name: "Desk", price: "100 PLN"},
  {name: "Computer", price: "5000 PLN"},
]

W jaki sposób możemy wyrenderować taką listę ShopItems? Robimy to przy użyciu funkcji map:

import React from 'react';
import ShopItem from './components/ShopItem';

function App() {
  const items = [
  {name: "Chair", price: "15 PLN"}, 
  {name: "Desk", price: "100 PLN"},
  {name: "Computer", price: "5000 PLN"},
]
  return (
    <>
      <ul>
          {items.map(item => {
            return <ShopItem name={item.name} price={item.price} />
          })}
      </ul>
    </>
  );
}

export default App;

Możemy jeszcze dodać warunek, aby lista była renderowana tylko wtedy, gdy nie jest pusta:

import React from 'react';
import ShopItem from './components/ShopItem';

function App() {
  const items = [
  {name: "Chair", price: "15 PLN"}, 
  {name: "Desk", price: "100 PLN"},
  {name: "Computer", price: "5000 PLN"},
]
  return (
    <>
      <ul>
          {items.length ? items.map(item => {
            return <ShopItem name={item.name} price={item.price} />
          }) : null}
      </ul>
    </>
  );
}

export default App;

Aby zobaczyć, jak działa nasz ternary operator i null, który przekazujemy, gdy items jest puste, wykomentujmy sobie nasze itemy:

import React from 'react';
import ShopItem from './components/ShopItem';

function App() {
  const items = [
  // {name: "Chair", price: "15 PLN"}, 
  // {name: "Desk", price: "100 PLN"},
  // {name: "Computer", price: "5000 PLN"},
]
  return (
    <>
      <ul>
          {items.length ? items.map(item => {
            return <ShopItem name={item.name} price={item.price} />
          }) : null}
      </ul>
    </>
  );
}

export default App;

Teraz nie pokazuje się nic. Oczywiście, możemy to zmienić i dać coś do wyrenderowania w przypadku pustego items:

import React from 'react';
import ShopItem from './components/ShopItem';

function App() {
  const items = [
  // {name: "Chair", price: "15 PLN"}, 
  // {name: "Desk", price: "100 PLN"},
  // {name: "Computer", price: "5000 PLN"},
]
  return (
    <>
      <ul>
          {items.length ? items.map(item => <ShopItem name={item.name} price={item.price} />) : <li>No items available!</li>}
      </ul>
    </>
  );
}

export default App;

Techniki poznane dzisiaj z pewnością przydadzą się w nadchodzącym projekcie, jakim będzie napisanie aplikacji ToDo List w bibliotece React. Może się tak nie wydawać, ale po przeczytaniu tego artykułu mamy de facto wszystkie potrzebne umiejętności do zrobienia takiego projektu.