Przypominamy sobie podstawowe hooki Reacta – useState, useEffect, useRef oraz useReducer. Uczymy się korzystać z API zewnętrznych oraz instalować i wykorzystywać zewnętrzne biblioteki Reacta. Przypominamy sobie własne komponenty oraz ich propsy.

Hook useState – prosty counter

Ugruntujemy sobie wiedzę na temat useState tworząc kolejną aplikację typu licznik. Robimy to po to, aby później zobaczyć to samo z hookiem useReducer i zauważyć różnice i podobieństwa.

Na początek tworzymy aplikację komendą:

npx create-react-app counter-app

Następnie przechodzimy tam poprzez cd (change directory):

 cd counter-app

Teraz import useState w App.js:

import React, { useState } from 'react';

W funkcji App, przed returnem używamy hooka:

const [count, setCount] = useState(0);

Następnie tworzymy funkcje increment i decrement:

function increment() {
      setCount(count+1);
    }
    function decrement(){
      setCount(count-1);
    }

W returnie upraszczamy markup i wyświetlamy count poprzez interpolację:

return (
    <div className="App">
      <h1>Count: {count}</h1>
    </div>
  );

Teraz nasze przyciski:

return (
    <div className="App">
      <h1>Count: {count}</h1>
      <button onClick={() => increment()}>Increment</button>
      <button onClick={() => decrement()}>Decrement</button>
    </div>
  );

Możemy odpalić przy pomocy komendy:

npm start

Jak widać nic specjalnego – zwykły licznik. Już to robiliśmy. Warto jednak wspomnieć, że stan początkowy może być wyliczany przy pomocy jakiejś funkcji. Na StackOverflow znalazłem taką funkcję losującą liczbę z zakresu (tak, JS nie ma tego wbudowanego):

function randomIntFromInterval(min, max) { // min and max included 
  return Math.floor(Math.random() * (max - min + 1) + min);
}

Chciałbym to sobie dodać do naszej funkcji App i za każdym razem losować stan początkowy licznika. Dodanie funkcji trudne nie jest, wystarczy przekopiować:

function App() {
    function randomIntFromInterval(min, max) { // min and max included 
    return Math.floor(Math.random() * (max - min + 1) + min);
  }

Użycie tej funkcji w useState też trudne nie jest, zgodnie z Reactową konwencją zwracamy funkcję strzałkową, która zwraca wynik jakiejś operacji:

const [count, setCount] = useState(() => {
      return randomIntFromInterval(1,6);
});

Teraz stan początkowy będzie zawsze losowany. Możemy to sprawdzić odświeżając kilka razy.

Hook useEffect – różne konfiguracje

Doimportujmy sobie useEffect na dobry początek:

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

Przypomnę – useEffect z pustą tablicą zależności wykonuje się tylko raz, po pierwszym dodaniu komponentu do strony:

useEffect(() => {
      console.log("component rendered");
    }, []);

To wyświetli nam informację 1 razy, gdy komponent zostanie dodany (2 razy w trybie deweloperskim ze względu na React.StrictMode…)

Przekazanie zależności będzie wywoływało efekt za każdym razem, gdy zależność się zmieni:

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

Świetnie się to nadaje do logowania zmiany stanu bez żadnych komplikacji (wrzucenie takiego loga do funkcji handleClick, zaraz po setState wcale nie jest takim dobrym pomysłem, nie musi i najprawdopodobniej nie będzie poprawnie logować) oraz do różnego rodzaju efektów, które mają się wykonać po zmianie stanu właśnie.

useState i useEffect z pustą tablicą zależności możemy użyć, aby zaciągnąć coś z API po dodaniu komponentu a następnie wyświetlić to w naszej aplikacji:

function App() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('https://api.chucknorris.io/jokes/random')
      .then(response => response.json())
      .then(json => setData(json))
      .catch(error => console.error(error));
  }, []);

  return (
    <div>
      {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <div className="loader"></div>}
    </div>
  );
}

export default App;

Hook useEffect nadaje się też do różnego rodzaju interwałów, pamiętajmy tylko o tym, aby zwrócić funkcję, która czyści interwał:

function increment() {
      setCount(count+1);
    }
function decrement(){
      setCount(count-1);
}
useEffect(() => {
      let interval = setInterval(() => increment(), 1000);
      return () => clearInterval(interval);
    }, [increment]);

Hook useReducer – nowy hook

Rzućmy okiem raz jeszcze na najbardziej uproszczoną wersję naszego licznika:

import React, { useState} from 'react';
function App() {
    
    const [count, setCount] = useState(0);
    function increment() {
      setCount(count+1);
    }
    function decrement(){
      setCount(count-1);
    }
   
    
  return (
    <div className="App">
      <h1>Count: {count}</h1>
      <button onClick={() => increment()}>Increment</button>
      <button onClick={() => decrement()}>Decrement</button>
    </div>
  );
}

Myślę, że najłatwiej będzie pokazać, jak działa useReducer na przykładzie. Po pierwsze, odpowiedni import:

import React, { useReducer} from 'react';

Teraz funkcja reducer (może być top-level, nad App):

function reducer(state, action) {
  if(action.type === 'increment') {
    return {
      count: state.count + 1
    };
  }
  else if(action.type === 'decrement') {
    return {
      count: state.count -1
    };
  }
  throw Error('Unknown action.');
}

Już chyba zaczynamy widzieć, do czego to zmierza. Teraz używamy useReducer:

function App() {
    
  const [state, dispatch] = useReducer(reducer, { count: 0 });

Ten hook przyjmuje funkcję reducer oraz stan (jako obiekt), zwraca zaś stan i dispatch. Teraz możemy wyświetlić nasz stan oraz użyć dispatch z odpowiednią akcją w naszych przyciskach:

import React, { useReducer} from 'react';

function reducer(state, action) {
  if(action.type === 'increment') {
    return {
      count: state.count + 1
    };
  }
  else if(action.type === 'decrement') {
    return {
      count: state.count -1
    };
  }
  throw Error('Unknown action.');
}

function App() {
    
  const [state, dispatch] = useReducer(reducer, { count: 0 });
    
   
  return (
    <div className="App">
      <h1>Count: {state.count}</h1>
      <button onClick={() => dispatch({type: 'increment'})}>Increment</button>
      <button onClick={() => dispatch({type: 'decrement'})}>Decrement</button>
    </div>
  );
}

Hook useRef – referencja do obiektu

Hook useRef tworzy nam taką referencję/handle do jakiegoś elementu JSX. Pozwolę sobie wkleić przykład z pierwszej części tutoriala:

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;

Komponent FakeForm, referencja/handle do elementu <input>. Tak tworzymy „uchwyt” do naszego <input>. Teraz możemy z niego skorzystać i po wciśnięciu przycisku wylogować w konsoli wartość naszego <input>:

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;

Możemy też po wylogowaniu wyczyścić nasz input – ref to pełnoprawny uchwyt i może tak odczytywać jak i nadpisywać:

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;

Własny komponent i props – LoremItem

Tworzymy własny komponent (w App.js, nad App), który będzie wyświetlać nam „Lorem Ipsum”. Krok pierwszy:

function LoremItem(props) {
  const loremText = "Lorem ipsum dolor sit amet consectetur adipisicing elit. Iure veniam sed est rem quam ab a quis? Ipsum in, dolore ad eligendi ex maxime consequuntur quasi laudantium omnis? Dignissimos, ab!"
  
}

Przyjmuje propsy, posiada loremText. W propsach będzie długość tego loremText zapisana. Teraz pora włączyć obsługę stanu:

function LoremItem(props) {
  const loremText = "Lorem ipsum dolor sit amet consectetur adipisicing elit. Iure veniam sed est rem quam ab a quis? Ipsum in, dolore ad eligendi ex maxime consequuntur quasi laudantium omnis? Dignissimos, ab!"
  const [text,] = useState('');
  return (
    <p>{text}</p>
  )
}

Już zaczyna to jakoś wyglądać. setText nie będziemy używać, więc je pominęliśmy.

Teraz chcemy użyć callback w useState, aby przekazać wartość początkową, wyliczaną za pomocą props.num. Tyle razy ma powtórzyć loremText, ile razy mamy w props.num:

function LoremItem(props) {
  const loremText = "Lorem ipsum dolor sit amet consectetur adipisicing elit. Iure veniam sed est rem quam ab a quis? Ipsum in, dolore ad eligendi ex maxime consequuntur quasi laudantium omnis? Dignissimos, ab!"
  const [text,] = useState(() => {
    let num = props.num;
    let text = '';
    while(num) {
      text += loremText;
      num--;
    }
    return text;
  });
  
  return (
    <p>{text}</p>
  )
}

Używamy tego tak (w App.js w returnie funkcji App):

<LoremItem num={3}/>

Przyda nam się do następnego przykładu.

Instalowanie zewnętrznych bibliotek – Scroll Into View

Nasz App.js powinien teraz wyglądać tak:

import './App.css';
import React, { useState} from 'react';

function LoremItem(props) {
  const loremText = "Lorem ipsum dolor sit amet consectetur adipisicing elit. Iure veniam sed est rem quam ab a quis? Ipsum in, dolore ad eligendi ex maxime consequuntur quasi laudantium omnis? Dignissimos, ab!"
  const [text,] = useState(() => {
    let num = props.num;
    let text = '';
    while(num) {
      text += loremText;
      num--;
    }
    return text;
  });
  
  return (
    <p>{text}</p>
  )
}
function App() {

  return (
    <div className="App">
      <LoremItem num={9}/>
      <LoremItem num={7}/>
      <LoremItem num={7}/>
      <LoremItem num={9}/>
      <LoremItem num={12}/>
      <LoremItem num={9}/>
      <LoremItem num={12}/>
    </div>
  );
}

export default App;

Zainstalujemy sobie biblioteczkę ScrollIntoView. Jeżeli mamy włączony npm start, to go wyłączmy (CTRL+C oraz y, enter) i odpalmy poniższą komendę:

npm i --save react-scroll-into-view

Teraz dokonujemy potrzebnego importu:

import ScrollIntoView from 'react-scroll-into-view';

Dodajemy guzik do scrollowania na sam dół opleciony wrapperem, który zaimportowaliśmy:

<div className="App">
      <ScrollIntoView selector="#footer">
      <button>Jump to bottom</button>
      </ScrollIntoView>

Teraz gdzieś na samym dole (przed zamykającym divem) dodamy element, o id „footer”, do którego będziemy mieli element płynnego scrollowania po wciśnięciu przycisku:

<div id="footer">Scroll target element</div>
</div>
);

Teraz po wykonaniu npm start możemy zobaczyć akcję płynnego przechodzenia do stopki po wciśnięciu naszego przycisku. A czy dalibyśmy radę zrobić odwrotność tego? Płynne przejście ze stopki do góry?

Po pierwsze, musimy nadać id naszemu przyciskowi:

<ScrollIntoView selector="#footer" >
      <button id="up">Jump to bottom</button>
 </ScrollIntoView>

Po drugie, użyć wrappera na dole, z odpowiednim atrybutem selector:

<ScrollIntoView selector="#up">
<div id="footer">Scroll target element</div>
</ScrollIntoView>
</div>
);

Działa. W przyszłości często będziemy korzystać z różnych zewnętrznych bibliotek Reacta.