1. რა არის React ?

React-ი არის, ჯავასკრიპტის ბიბლიოთეკა (და არა ფრეიმვორკი) ღია წყაროთი, რომელიც გამოიყენება სამომხმარებლო ინტერფეისების (UI - User Interface) შესაქმნელად. ბიბლიოთეკა შექმნილია კომპანია Facebook-ის მიერ.

სამომხმარებლო ინტერფეისი (UI - User Interface) არის სივრცე, გარემო, რომლის მეშვეობითაც იმართება პროგრამული მოწყობილობები და აპლიკაციები. ვებ-სივრცის გადმოსახედიდან თუ ვიტყვით, სამომხმარებლო ინტერფეისია ყველაფერი, რასაც ბრაუზერში ვხედავთ.

რა განსხვავებაა ფრეიმვორკსა და ბიბლიოთეკას შორის ?

ბიბლიოთეკა არის კონკრეტული მიზნებისათვის შექმნილი კონკრეტული ფუნქციების ორგანიზებული ნაკრები (მაგ: სტრიქონებთან სამუშო ფუნქციების, ფაილებთან სამუშაო ფუნქციების და ა.შ).

ფრეიმვორკი არის პროექტის აბსტრაქტული, განზოგადებული ფორმა, შაბლონი, ჩონჩხი, კოდირების დიზაინი, რომელშიც სისტემის მართვის მექანიზმი უკვე შექმნილია, თუმცა არის სივრცეები სადაც შეგვიძლია ჩვენი კოდი შევიტანოთ, შემდეგ ხდება ზემოთ ნახსენები, უკვე არსებული ფუნდამენტისა და ჩვენი კოდის 'შეზავება' და ასე ყალიბდება პროექტის სრული იერსახე.

ვიდრე რეაქტის შესწავლას დავიწყებთ, სასურველია გარკვეული წარმოდგენა გვქონდეს შემდეგ საკითხებზე : HTML, CSS , ჯავასკრიპტის საფუძვლები, როგორ მუშაობს ასინქრონული ჯავასკრიპტი ?

ნებისმიერი ვებ-გვერდი, შეიძლება განხილულ იქნას, როგორც კონკრეტული ფუნქციონალების, ფრაგმენტების, კომპონენტების ერთობლიობა (უცხო სიტყვ. ლექსიკონი : კომპონენტი - რისამე შემადგენელი ნაწილი). კომპონენტის უკან შეიძლება მოიაზრებოდეს საიტის ქუდი (header), გვერდითი მენიუ (sidebar), ძირითადი შიგთავსი და ა.შ.


(ფოტოს წყარო : https://miro.medium.com/v2/resize:fit:720/format:webp/1*V3ZOFh5Ed4MLCIgi6FnLmA.jpeg)

სწორედ სამომხმარებლო ინტერფეისის კომპონენტებად დაყოფაა რეაქტის ძირითადი პრინციპი, რომელიც საშუალებას გვაძლევს ვწეროთ მოდულარული, უკეთ ორგანიზებული და მრავალჯერადად გამოყენებადი კოდი. ყველა კომპონენტის უკან დგას მხოლოდ ამ კომპონენტის გამართულად მუშაობაზე პასუხისმგებელი ინსტრუქციები, რომლებიც სრულიად იზოლირებულნი არიან დანარჩენი კოდისაგან და სწორედ ეს განაპირობებს ნახსენებ ორგანიზებულობასა და მოდულარულობას. შეიძლება ითქვას რომ რეაქტში დაწერილი აპლიკაცია წარმოადგენს კომპონენტების იერარქიულ ხეს.

2. პროექტის გამართვა, ჩვენი პირველი აპლიკაცია

რა არის Node.js ?

Node ანუ Node.js — არის V8 (ავტ. Google) ძრავზე დაფუძნებული პროგრამული პლატფორმა, რომელიც ჯავასკრიპტს, როგორც სპეციალიზირებულ ენას, გარდაქმნის პროგრამირების სრულფასოვან და საერთო დანიშნულების ენად.

Node.js ის გადმოწერა შესაძლებელია ოფიციალური ვებ-გვერდიდან. ინსტალაცია მიმდინარეობს სტანდარტულად, ყოველგვარი სირთულეების გარეშე. იმისათვის, რათა დავრწმუნდეთ, რომ ინსტალაციამ წარმატებით ჩაიარა, ბრძანებათა ველიდან გავუშვათ შემდეგი ბრძანება :

node –v                  
                

რა არის npm ?

npm (Node.js Package Manager) არის Node.js პაკეტების მენეჯერი. პაკეტის მიღმა იგულისხმება იმ ფაილების ნაკრებები, რომლებიც გვჭირდება კონკრეტული მოდულის შესაქმნელად. npm ავტომატურად ინსტალირდება Node.js-ის ინსტალაციისას.

npm-ის ვერსიის გასაგებად ბრძანებათა ველიდან გავუშვათ შემდეგი ბრძანება :

npm -v
                

რა არის Vite ?

Vite არის დღესდღეობით ძალიან პოპულარული ხელსაწყო, რომელიც გამოიყენება ისეთ ბიბლიოთეკებთან და ფრეიმვორკებთან სამუშაოდ როგორებიცაა React, Vue და ა.შ. მისი მეშვეობით სწრაფად და მარტივად იქმნება პროექტის საწყისი მონახაზი და ჩვენ აღარ გვიწევს სხვადასხვა კონფიგურაციული ფაილებისა და საქაღალდეთა იერარქიების შექმნა. დასახელება 'Vite' მომდინარეობს ფრანგული სიტყვიდან - 'Vite' (ჟღერს როგორც /vit/), რაც ნიშნავს სწრაფს.

ბრძანებათა ველის მეშვეობით გადავინაცვლოთ ჩვენთვის სასურველ საქაღალდეში და გავუშვათ ბრძანება :

npm create vite@latest my-react-app
                


ახლა გავუშვათ შემდეგი ბრძანებები :

cd my-react-app
npm install
npm run dev
                

შედეგად, სასურველ საქაღალდეში შეიქმნება პროექტი შემდეგი სტრუქტურით :



შევიდეთ მისამართზე : http://localhost:5173/

3. პროექტის სტრუქტურა და მუშაობის ძირითადი პრინციპები

როგორც უკვე ვთქვით, დაინსტალირებულ პროექტს აქვს შემდეგი სტრუქტურა :



node_modules

როდესაც npm-ის მეშვეობით ვაინსტალირებთ სხვადასხვა პაკეტებს, სწორედ node_modules საქაღალდეში იწერება ამ პაკეტებისათვის საჭირო ფაილები, დამოკიდებულებები და ა.შ. Node.js-ის მიერ. ამ საქაღალდეში ჩარევა არასდროს დაგვჭირდება (წესით 😋 :)), Node.js ყველაფერს თვითონ მიხედავს 🤩

public საქაღალდე

ამ საქაღალდეში ინახება პროექტის საჯარო ინსტრუმენტები : ფოტოები, ფონტები, ხატულები და ა.შ.

src საქაღალდე

ყველაზე ხშირად სწორედ ამ საქაღალდეში მოგვიწევს სტუმრობა პროექტზე მუშაობისას 😌 აქ არის განთავსებული ძირითადი კოდი : კომპონენტები, ბიზნეს-ლოგიკები, სტილები და ყველაფერი, რაც პროექტის აგებისასაა (build) საჭირო.

***

როდესაც პროექტის აგებაზე ვსაუბრობთ, აუცილებლად უნდა აღინიშნოს რომ ეს პროცესი შეიძლება წარიმართოს ორ სხვადასხვა გარემოში : ერთი არის სამუშაო, ანუ ის გარემო, რომელშიც ვქმნით პროექტს (development), ჩვენი ლოკალური სერვერი, და მეორე - წარმოებაში ჩაშვებული პროექტის გარემო (production), რეალური სერვერი. ის პროცესები, რომლებიც პროექტის აგების მიღმა იგულისხმება, ორივე მათგანში სხვადასხვანაირად მიმდინარეობს და ამას აქვს თავისი მიზეზები.

სამუშაო გარემო

სამუშაო გარემოში პროექტის ასამუშავებლად უნდა გავუშვათ შემდეგი ბრძანება :

npm run dev
                

როდესაც ამ ბრძანებას ვუშვებთ, Vite ჩვენს კომპიუტერს იყენებს, როგორც ლოკალურ სამუშაო სერვერს : დავუშვათ ბრაუზერში გავხსენით რაიმე ბმული, ბუნებრივია ამ ბმულის მუშაობისათვის საჭიროა კონკრეტული კოდი, სხვადასხვა რესურსები და ა.შ, Vite იღებს ამ ყველაფერს ჩვენს კომპიუტერში არსებული ჩვენი პროექტიდან და აწვდის ბრაუზერს. სხვა სიტყვებით თუ ვიტყვით - Vite წარმოადგენს ერთგვარ ხიდს ბრაუზერსა და კოდს შორის.

რა არის HMR ?

ერთ-ერთი ყველაზე მნიშვნელოვანი, რაც ამ დროს ხდება, არის ე.წ HMR (Hot Module Replacement), ანუ მოდულების პირდაპირ რეჟიმში განახლების პროცესი : როდესაც კონკრეტულ მოდულში რაიმეს ვცვლით, ეს ცვლილება დაუყოვნებლივ აისახება ბრაუზერში, არ არის საჭირო გვერდის ხელახალი ჩატვირთვა (refresh). ეს ხელს უწყობს მუშაობის პროცესის ასწრაფებას.

ასევე უნდა აღინიშნოს რომ Vite ჩვენი კოდის კომპილაციას ახდენს საჭიროებიდან გამომდინარე - აკომპილირებს მხოლოდ მაშინ და მხოლოდ იმას, როცა და რასაც მოითხოვს ბრაუზერი. შესაბამისად პროცესები საკმაოდ სწრაფად მიმდინარეობს.

რეალური გარემო

პროექტის რეალურ გარემოში განთავსებამდე საჭიროა მისი 'მომზადება', ერთგვარი გარდაქმნა. ამისათვის გამოიყენება შემდეგი ბრძანება :

npm run build
                

ბრძანების გაშვების შემდეგ ხდება შემდეგი პროცესები :

  1. კოდის კომპლექტაცია:

    • სადაც შესაძლებელია Vite ჩვენს კოდს აერთიანებს ოპტიმიზირებულ JavaScript ფაილებში. ეს ამცირებს აპლიკაციის ჩატვირთვისათვის საჭირო რესურსების HTTP მოთხოვნების რაოდენობას.
  2. კოდის მინიფიცირება:

    • ხდება JavaScript და CSS ფაილების მინიფიცირება, კოდში იშლება კომენტარები და არასაჭირო გამოტოვებული ხაზები. ეს ამცირებს ფაილების ზომას, რაც ზრდის მათი ჩატვირთვის სისწრაფეს.
  3. კოდის ოპტიმიზაცია:

    • მას შემდეგ რაც დარწმუნდება რომ ყველაფერი საჭირო ადგილზეა, Vite შლის გამოუყენებელ კოდს საფინალო ოპტიმიზირებული ფაილების შექმნამდე. ესეც, რა თქმა უნდა, ამცირებს ფაილების ზომებს.
  4. კოდის დაყოფა:

    • ოპტიმიზირებულ კოდს Vite ყოფს პატარა ფრაგმენტებად, რომლებიც ჩაიტვირთებიან საჭიროებიდან გამომდინარე. ეს ნიშნავს რომ თავდაპირველი ჩატვირთვისას გაეშვება მხოლოდ კრიტიკულად საჭირო კოდი და არაფერი სხვა. რაც შეამცირებს ჩატვირთვის დროს.
  5. რესურსების ოპტიმიზაცია:

    • ხდება ფოტოების, ფონტების და სხვა სტატიკური რესურსების ოპტიმიზაცია. იკუმშება ფოტოების ზომები, ქეშის ეფექტურად მუშაობისათვის იქმნება რესურსების ჰეშირებული დასახელებები. ეს გულისხმობს შემდეგს: როდესაც კონკრეტული რესურსის შიგთავსი ან წყარო იცვლება და პროექტის აგება (build) ხელახლა ხდება, მას ავტომატურად ენიჭება ახალი ჰეშირებული სახელი, რაც იმას ნიშნავს რომ ბრაუზერს არ მიეცემა საშუალება ჩატვირთოს ძველი, დაქეშილი ვერსია (ალბათ გქონიათ შემთხვევა როდესაც CSS-ში შეგიცვლიათ რაიმე, მაგრამ ეს ცვლილება არ ასახულა ბრაუზერში და დაგჭირვებიათ გვერდის სრული განახლება (hard refresh), ან რესურსისათვის სახელის გადარქმევა, ან ვერსიის მითითება : style.css?v=1).
  6. dist საქაღალდე:

    • იქმნება dist საქაღალდე და ყოველივე ეს ინახება მასში (ინგ: distribution - საბოლოო მყიდველამდე, კლიენტამდე ან მომხმარებლამდე მიტანა). სწორედ dist საქაღალდის შიგთავსი იტვირთება ხოლმე სერვერზე და ჩვენ ამ ყველაფერს პრაქტიკულ განყოფილებაში ვნახავთ, როდესაც რეალურ პროექტს შევქმნით 😍

***

დავუბრუნდეთ src საქაღალდეს 😍. მასში არსებულ assets საქაღალდეში ინახება პროექტის სტატიკური რესურსები : ფოტოები, ფონტები, სტილები და ა.შ. ტერმინი 'სტატიკური' მიანიშნებს იმაზე, რომ, როგორც წესი, ეს ფაილები ყველა მომხმარებლისათვის ერთნაირია ხოლმე და არ ესაჭიროებათ რაიმე სერვერული დამუშავება ბრაუზერში გაგზავნამდე.

src საქაღალდეში არსებული ძირითადი ფაილებია :

main.tsx

ეს არის აპლიკაციის შესავალი წერტილი სადაც ხდება პროექტის ინიციალიზაცია, ძირითადი კომპონენტის განსაზღვრა (ხშირად ეწოდება App) და სხვადასხვა გლობალური კონფიგურაციული პარამეტრების აღწერა. სწორედ App კომპონენტია ხოლმე კომპონენტთა იერარქიული ხის ძირითადი ფესვი, ყველა კომპონენტის მამა ☝ 😀 :

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
  <App />
  <React.StrictMode>,
)
                

<React.StrictMode> არის კომპონენტი, რომელიც გარს აკრავს ჩვენს პროექტს და რომელიც გვეხმარება ვწეროთ უკეთესი და უფრო გამართული კოდი: გვატყობინებს მოსალოდნელი უზუსტობების, ასევე ხარვეზების შესახებ და ა.შ. აღსანიშნავია რომ კომპონენტი ვიზუალური მხარის გენერირებაში არ ღებულობს მონაწილეობას, იგი მხოლოდ დეველოპერული ხელსაწყოა.

root იდენტიფიკატორი დაკავშირებულია პროექტის ძირ საქაღალდეში არსებულ - index.html ფაილში აღწერილ ძირითად ელემენტთან :

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/vite.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Vite + React + TS</title>
    </head>
    <body>
        <div id="root"></div>
        <script type="module" src="/src/main.tsx"></script>
    </body>
</html>                    
                

ეს არის ერთგვერდიანი აპლიკაციების (SPA-Single Page Application) მუშაობის პრინციპის უმნიშვნელოვანესი ფრაგმენტი.

რა არის SPA ?

ერთგვერდიან აპლიკაციაში მოიაზრება ვებ-აპლიკაცია, რომელსაც ბრაუზერის განახლების გარეშე (refresh), მომხმარებლის ქმედებიდან გამომდინარე დინამიკურად გამოაქვს ინფორმაცია ერთ კონკრეტულ გვერდზე (index.html). სწორედ ეს მოიაზრება ტერმინ 'ერთგვერდიანი'-ს უკან და არა ის რომ SPA ტიპის აპლიკაციებში მხოლოდ ერთი გვერდის გახსნა შეგვიძლია. სხვა სიტყვებით თუ ვიტყვით: სერვერზე გვაქვს ერთადერთი გვერდი (index.html), როდესაც მომხმარებელი აკეთებს რაიმე მოთხოვნას, SPA მომხმარებლის მხარეს ანუ ბრაუზერში აგენერირებს შესაბამის ახალ შიგთავსს და ანახლებს აღნიშნულ გვერდს.

ჩვენი პროექტი ბრაუზერში გამოიყურება ამგვარად :


ხოლო თუ პროექტის კოდს ვნახავთ (Ctrl+U), დაგვხვდება ამდაგვარი სურათი :


როგორც ვხედავთ, ვიზუალური მხარე აშკარად არ შეესაბამება კოდს. მაშ რა ხდება ? როგორ ვხედავთ ბრაუზერში იმას, რასაც ვხედავთ ? მივყვეთ პროცესებს ნაბიჯ-ნაბიჯ.

index.html

index.html ფაილში აღწერილია საბაზისო მეტა თეგები, ხატულას ბმული (icon), გვერდის სათაური (title), ცარიელი div ელემენტი id="root" იდენტიფიკატორით და /src/main.tsx ფაილთან დაკავშირებული <script> თეგი.

id='root' ელემენტი წარმოადგენს წინასწარ განსაზღვრულ ერთგვარ სივრცეს, კონტეინერს, რომელშიც თავსდება, React კომპონენტების იერარქიული ხისაგან შექმნილი DOM-ი (DOM-ის შესახებ დაწვრილებითი ინფორმაცია შეგიძლიათ იხილოთ ჯავასკრიპტის კურსის მე-20 თავში).

როდესაც პროექტს ვხსნით ბრაუზერში, იგი სერვერიდან ტვირთავს index.html ფაილს :

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/vite.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Vite + React + TS</title>
    </head>
    <body>
        <div id="root"></div>
        <script type="module" src="/src/main.tsx"></script>
    </body>
</html>                    
                

შემდეგ ბრაუზერი ასრულებს /src/main.tsx ფაილში აღწერილ კოდს :

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
  <App />
  <React.StrictMode>,
)
                

ReactDOM.createRoot და root.render() მეთოდების გამოძახებისას, App კომპონენტისა და მისი შვილობილი კომპონენტებისაგან გენერირდება ვირტუალური DOM-ი და ინახება კომპიუტერის მეხსიერებაში, შემდეგ ვირტუალური DOM-ის საფუძველზე გენერირდება რეალური DOM-ი, რომელიც ჯდება id="root" ელემენტში და ბრაუზერში ვხედავთ შესაბამის შედეგს. სწორედ მეხსიერებაში შენახული რეალური DOM-ია ის, რასაც ბრაუზერში ვხედავთ.

შეიძლება გაჩნდეს კითხვები : რა საჭიროა ორი ტიპის DOM-ის გენერირება ? რატომ არ იქმნება პირდაპირ რეალური DOM-ი ?

საქმე იმაშია რომ ვირტუალური DOM-ი არის რეალური DOM-ის გამარტივებული, 'შემსუბუქებული' ვერსია. რას ნიშნავს ეს ?

ნებისმიერ DOM ელემენტს გააჩნია სხვადასხვა თვისებები : innerHTML, outerHTML, textContent, nodeType, childNodes.. რომლებიც მოიცავენ დეტალურ ინფორმაციას ელემენტისა და მისი შიგთავსის შესახებ. ელემენტებს ასევე გააჩნიათ სხვადსხვა ატრიბუტები : id, class, src, href, style, data-*..

ამ ყოველივეს ნაცვლად, ვირტუალურ DOM-ში ინახება მხოლოდ ის აუცილებელი ინფორმაცია რომელიც, საჭიროების შემთხვევაში, ბრაუზერში ცვლილებების მოსახდენად იქნება გამოყენებული. მაგალითად ელემენტთა ტიპები: div, span, input.. ან საბაზისო ატრიბუტები : id, class, value..

როდესაც რომელიმე კომპონენტის მდგომარეობა (state) იცვლება, რეაქტი ანახლებს ვირტუალურ DOM-ს, შემდეგ ადარებს ახალ და ძველ ვერსიებს და აფიქსირებს ცვლილებებს (ამ პროცესს რეკონსილაციას უწოდებენ ხოლმე, იგივე 'diffing' ინგლისურენოვან წყაროებში). ბოლოს კი, ნაცვლად მთლიანი რეალური DOM-ის ხელახალი გენერირებისა, მასში შეაქვს მხოლოდ დაფიქსირებული მინიმალური ცვლილებები.

რეალურ DOM-თან შედარებით, ვირტუალურ DOM-ში უფრო სწრაფად ხდება ძველი და ახალი ვერსიების შედარებაც და ცვლილებების შეტანაც, სწორედ ეს განაპირობებს მისი არსებობის საჭიროებას.

მაგალითად, თუ App კომპონენტს ასეთი სახე ექნება :

function App() {
    return (
        <div>
            <h1>გამარჯობა!</h1>
            <p>კეთილი იყოს თქვენი მობრძანება.</p>
        </div>
    );
}                      
                

მის საფუძველზე დაგენერირდება ასეთი ვირტუალური DOM :

{
    type: 'div',
    props: {
        children: [
            { type: 'h1', props: { children: 'გამარჯობა!' } },
            { type: 'p', props: { children: 'კეთილი იყოს თქვენი მობრძანება.' } }
        ]
    }
}                                         
                

რეალურ DOM-ს კი ექნება ასეთი სახე :

<div id="root">
    <div>
        <h1>გამარჯობა!</h1>
        <p>კეთილი იყოს თქვენი მობრძანება.</p>
    </div>
</div>                                    
                

რა არის CSR ?

პროცესს, რომელიც ზემოთ აღვწერეთ, CSR (Client-Side Rendering) ეწოდება. CSR მიდგომისას სერვერიდან იტვირთება ერთადერთი ფაილი მინიმალური სტრუქტურითა და ძირითადი ელემენტით (id='root'), ინტერფეისის გენერირებასთან დაკავშირებული ყველა სხვა პროცესი კლიენტის მხარეს - ბრაუზერში მიმდინარეობს.

CSR-ის ძირითადი უპირატესობა ისაა რომ ბრაუზერში ცვლილებების ასახვა, გვერდის ხელახალი ჩატვირთვის (refresh) გარეშე ხდება, თანაც იცვლება მხოლოდ საჭირო ელემენტების მდგომარეობა (state), რაც ამ მიდგომის სისწრაფეს განაპირობებს.

App.tsx

ფაილში აღწერილია პროექტის ძირითადი კომპონენტი - App. როგორც წესი, ეს კომპონენტი მოიცავს ხოლმე ყველა სხვა კომპონენტს, ანუ არის კომპონენტთა იერარქიული ხის ძირი.

import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";

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

    return (
    <>
        <div>
            <a href="https://vitejs.dev" target="_blank">
                <img src={viteLogo} className="logo" alt="Vite logo" />
            </a>
            <a href="https://react.dev" target="_blank">
                <img src={reactLogo} className="logo react" alt="React logo" />
            </a>
        </div>
        <h1>Vite + React</h1>
        <div className="card">
            <button onClick={() => setCount((count) => count + 1)}>
                count is {count}
            </button>
            <p>
                Edit <code>src/App.tsx</code> and save to test HMR
            </p>
        </div>
        <p className="read-the-docs">
            Click on the Vite and React logos to learn more
        </p>
    </>
    );
}

export default App;                                                       
                

სწორედ ამ კოდის შედეგი ვიხილეთ http://localhost:5173/ მისამართზე შესვლისას.

***

უკვე განვიხილეთ /src საქაღალდე და ასევე პროექტის ძირ საქაღალდეში არსებული /index.html ფაილი. გავაგრძელოთ ფაილთა განხილვა.

.eslintrc.cjs ფაილი

.eslintrc.cjs ფაილიში აღწერილია ESLint-ის კონფიგურაციული პარამეტრები :

module.exports = {
    root: true,
    env: { browser: true, es2020: true },
    extends: [
        'eslint:recommended',
        'plugin:@typescript-eslint/recommended',
        'plugin:react-hooks/recommended',
    ],
    ignorePatterns: ['dist', '.eslintrc.cjs'],
    parser: '@typescript-eslint/parser',
    plugins: ['react-refresh'],
    rules: {
        'react-refresh/only-export-components': [
            'warn',
            { allowConstantExport: true },
        ],
    },
}                                                       
                

რა არის ESLint ?

კომპიუტერულ მეცნიერებებში ტერმინი Lint აღნიშნავს ხელსაწყოს, რომელიც გამოიყენება სტატიკური კოდის ანალიზისა და სტილისტურ-სინტაქსური ხარვეზების აღმოჩენისათვის. ასეთ პროგრამებს ლინტერებს უწოდებენ ხოლმე. ESLint არის ჯავასკრიპტის ლინტერი, რომელიც გვეხმარება კოდის სწორად წერაში. იგი მიგვითითებს პროგრამულ და სინტაქსურ ხარვეზებზე, გვატყობინებს თუ სადმე ზედმეტი, გამოუყენებელი კოდი გვიწერია და ა.შ. სხვა სიტყვებით თუ ვიტყვით ESLint წააგავს მასწავლებელს, რომელიც წითელი კალმით გვისწორებდა ნაწერს ბავშვობაში 🧒🕵📝😁

ESLint-ს გააჩნია თავისი, საკუთარი წესები, თუმცა შესაძლებელია ჩავერიოთ ამ წესებში და ჩვენი საჭიროებისამებრ აღვწეროთ ისინი. სწორედ ამისათვის გამოიყენება .eslintrc.cjs ფაილი. განვიხილოთ მასში აღწერილი ძირითადი ატრიბუტები :

  • root: true - განსაზღვრავს რომ ეს არის ESLint-ის მთავარი კონფიგურაციული ფაილი. შედეგად ESLint აღარ ეძებს სხვა კონფიგურაციულ ფაილებს სხვა საქაღალდეებში.
  • env - განსაზღვრავს გარემოს, რომელშიც უნდა გაეშვას პროექტი. ჩვენს შემთხვევაში მითითებულია browser: true (მიუთითებს რომ პროექტი უნდა გაეშვას ბრაუზერში) და es2020: true (მიუთითებს რომ გამოყენებულ იქნება ES2020).
  • extends - მასივში აღწერილია ის კონფიგურაციები და წესთა წყობები, რომელთა მიხედვითაც ESLint იმუშავებს ჩვენს პროექტში.
  • ignorePatterns - განსაზღვრავს იმ ფაილებსა და საქაღალდეებს, რომლებიც ESLint-მა უნდა დააიგნოროს.
  • parser - განსაზღვრავს თუ რომელი ანალიზატორი უნდა გამოიყენოს ESLint-მა.

როგორ გავტესტოთ პროექტი ESLint-ის მეშვეობით ?

package.json ფაილის scripts სექციას თუ დავაკვირდებით, შევამჩნევთ ასეთ ჩანაწერს :

"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",                
                

ანუ ლინტერის გაშვება შეგვიძლია შემდეგი ბრძანებით :

npm run lint
                

ბრძანების შედეგად ტერმინალში გამოგვიჩნდება გაფრთხილებები დაშვებული უზუსტობების შესახებ. მაგალითად, ამ შემთხვევაში :


ვიხილავთ ამგვარ შეტყობინებას :


ლინტერი გვეუბნება რომ შეცდომა დაფიქსირდა კოდის ანალიზიზას (parsing).

თუ პირველ ფოტოს კარგად დავაკვირდებით შევამჩნევთ ასეთ რამეს :

const test = 'ტესტი'; // eslint-disable-line
                

ეს ჩანაწერი საშუალებას გვაძლევს ლინტერს დავაიგნორებინოთ სასურველი ხაზები, წინააღმდეგ შემთხვევაში ვიხილავდით კიდევ ერთ გაფრთხილებას გამოუყენებელი ცვლადის შესახებ.

თუ პროექტზე მუშაობისას ვერსიათა კონტროლის სისტემებსაც ვიყენებთ (git), კარგი იქნება თუ npm run lint ბრძანებას კომიტის გაკეთებამდე გავუშვებთ ხოლმე - დარწმუნებულნი ვიქნებით რომ უხარვეზო და გამართული კოდი მოხვდება კომიტში.

package.json ფაილი

package.json ფაილი შეიცავს ზოგად ინფორმაციას პროექტისა და მისი მუშაობისათვის საჭირო დამოკიდებულებების შესახებ :

{
    "name": "my-react-app",
    "private": true,
    "version": "0.0.0",
    "type": "module",
    "scripts": {
        "dev": "vite",
        "build": "tsc -b && vite build",
        "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
        "preview": "vite preview"
    },
    "dependencies": {
        "react": "^18.3.1",
        "react-dom": "^18.3.1"
    },
    "devDependencies": {
        "@types/react": "^18.3.3",
        "@types/react-dom": "^18.3.0",
        "@typescript-eslint/eslint-plugin": "^7.13.1",
        "@typescript-eslint/parser": "^7.13.1",
        "@vitejs/plugin-react": "^4.3.1",
        "eslint": "^8.57.0",
        "eslint-plugin-react-hooks": "^4.6.2",
        "eslint-plugin-react-refresh": "^0.4.7",
        "typescript": "^5.2.2",
        "vite": "^5.3.1"
    }
}           
                

განვიხილოთ ძირითადი ატრიბუტები :

  • "name": "my-react-app", - აპლიკაციის სახელწოდება, რომელიც განვსაზღვრეთ პროექტის ინსტალაციისას : npm create vite@latest my-react-app
  • "private" : true - ეს ნიშნავს რომ პროექტი დაცულია და შეუძლებელია მისი გამოქვეყნება npm-ის საჯარო საცავებში (public npm registry) - სივრცეებში, სადაც დეველოპერები ერთმანეთს უზიარებენ თავიანთ კოდებს, ბიბლიოთეკებს და ა.შ. როდესაც რაიმე პაკეტს ვაინსტალირებთ npm install ბრძანების მეშვეობით, როგორც წესი, სწორედ ამ საცავებიდან ხდება მისი გადმოწერა (ოფიციალური საცავი : https://www.npmjs.com). პ.ს. აქ რაიმე პროექტის ასატვირთად private ატრიბუტის მნიშვნელობად უნდა მივუთითოთ false, შემდეგ კი გავუშვათ npm publish ბრძანება, დანარჩენს თვითონ ტერმინალი გვეტყვის 😍
  • "version": "0.0.0", - ჩვენი აპლიკაციის მიმდინარე ვერსია.
  • scripts - ამ ატრიბუტში განსაზღვრულია ფსევდონიმები, რომელთა მეშვეობითაც მარტივდება რეაქტის სხვადასხვა ბრძანებებთან წვდომა ტერმინალიდან.
  • dependencies - აქ აღწერილია აპლიკაციის მუშაობისათვის საჭირო დამოკიდებულებები.
  • devDependencies - აქ აღწერილია პროექტზე მუშაობისას (development) საჭირო დამოკიდებულებები.

package-lock.json ფაილი

package-lock.json ფაილის ძირითადი დანიშნულებაა განსაზღვროს სხვადასხვა პაკეტებისა და დამოკიდებულებების, ასევე მათი ქვე-დამოკიდებულებების ზუსტი ვერსიები. ეს სიზუსტე განაპირობებს იმას რომ ყველას, ვინც კი ამ კონკრეტულ პროექტზე იმუშავებს, ექნება დამოკიდებულებათა ერთნაირი ვერსიები. ეს კი საკმაოდ ეფექტური მიდგომაა პროგრამირებაში არსებული მარადიული პრობლემის - "ჩემთან მუშაობს 😩" თავიდან ასარიდებლად 😁😁.

როდესაც ვქმნით პროექტს რომელიმე ხელსაწყოს მეშვეობით (მაგ: Vite), ჯერ გენერირდება package.json ფაილი და მასში იწერება ზოგადი ინფორმაცია პროექტისა და საჭირო დამოკიდებულებების შესახებ. Vite არჩევს ამ დამოკიდებულებების ბოლო სტაბილურ ვერსიებს და განსაზღვრავს ვერსიათა შუალედებს, ამისათვის იყენებს ამდაგვარ ჩანაწერებს :

...
"react": "^18.3.1",
"react-dom": "^18.3.1"
...
                

^18.3.1 ჩანაწერი გულისხმობს რომ პროექტი თავსებადია ბიბლიოთეკის ნებისმიერ ვერსიასთან, რომელიც მეტია ან ტოლი 18.3.1-ზე და ნაკლებია შემდეგ მაჟორულ ვერსიაზე : >= 18.3.1 და < 19.0.0.

როდესაც გავუშვებთ ინსტალაციის ბრძანებას - npm install, შეიქმნება package-lock.json ფაილი, npm მოძებნის მითითებულ შუალედებთან თავსებად, ბოლო მინორულ ვერსიებს (მაგ: 18.4.0) და დააფიქსირებს მათ package-lock.json ფაილში. თუ სხვა დეველოპერიც დააინსტალირებს პროექტს, ან ჩვენ წავშლით node_modules საქაღალდეს და ხელახლა გავუშვებთ ინსტალაციის ბრძანებას, npm გადაიკითხავს package-lock.json ფაილს და გამოიყენებს იქ მითითებულ ვერსიებს. იგივე მოხდება npm update ბრძანების გაშვებისასაც.

ეს ნიშნავს რომ ყოველთვის გვექნება წვდომა ბიბლიოთეკებში შეტანილ ბოლო მინორულ ცვლილებებზე. მაჟორულ ვერსიებზე ავტომატურად გადართვა არ ხდება შემდეგი მიზეზით : როგორც წესი, მაჟორულ ვერსიებს შორის საკმაოდ მნიშვნელოვანი და რადიკალური ცვლილებებია ხოლმე, რის გამოც, კონკრეტული ბიბლიოთეკის შემდეგი მაჟორული ვერსია შეიძლება არათავსებადი იყოს არსებულ კოდთან და ამან შეცდომები გამოიწვიოს.

ასე და ამგვარად, package-lock.json ფაილში გვექნება ასეთი სიტუაცია :

...

"node_modules/@types/react": {
    "version": "18.3.3",
    ...
},
"node_modules/@types/react-dom": {
    "version": "18.3.3",
    ...
},

...
                

როგორც ვხედავთ აქ უკვე ზუსტი ვერსიებია მითითებული.

tsconfig.app.json ფაილი

tsconfig.app.json არის TypeScript-ის კონფიგურაციული ფაილი, რომელიც განსაზღვრავს თუ როგორ დაამუშავოს TypeScript-მა ჩვენი კოდი და როგორ მოახდინოს მისი კომპილაცია.

tsconfig.json ფაილი

შესაძლებელია რომ პროექტის სხვადასხვა ნაწილებს ტაიპსკრიპტის კონფიგურაციის სხვადასხვანაირად გამართვა ესაჭიროებოდეთ. რა თქმა უნდა შეგვიძლია ეს ყველაფერი ერთ ფაილში აღვწეროთ, თუმცა მოდულარულობისა და უკეთ ორგანიზებულობის თვალსაზრისით, უმჯობესია თუ ცალ-ცალკე ფაილებში ვიზამთ ამას.

tsconfig.json არის ტაიპსკრიპტის კონფიგურაციული ფაილების 'გზამკვლევი', მასში აღწერილია სხვადასხვა tsconfig ფაილების ბმულები.

tsconfig.node.json ფაილი

tsconfig.node.json ფაილში აღწერილია Node.js-თან და Vite-სთან სამუშაო, ტაიპსკრიპტის კონფიგურაციული პარამეტრები. ეს პარამეტრები გამოიყენება პროექტის კორექტულად აგებისა და ტაიპსკრიპტისათვის დამახასიათებელი უმნიშვნელოვანი პროცესის - ტიპიზაციის სწორად წარმართვისათვის.

vite.config.ts

vite.config.ts ფაილში ხდება Vite-ს სამუშაო სქემის გამართვა.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [react()],
})                    
                

Vite-ს მეშვეობით ჩვენ React პროექტი დავაინსტალირეთ, ამიტომაც იტვირთება შესაბამისი პროგრამული გაფართოება.

4. კომპონენტის შექმნა, რა არის JSX ?

რეაქტში გვხვდება ორი სახის - კლასზე დაფუძნებული (Class component) და ფუნქციაზე დაფუძნებული (Functional component) კომპონენტები. დღესდღეობით ფუნქციაზე დაფუძნებული კომპონენტები უფრო პოპულარულია მათი სიმარტივის გამო. აღვწეროთ ჩვენი პირველი კომპონენტი, /src საქაღალდეში შევქმნათ Message.tsx ფაილი შემდეგი კოდით :

// PascallCase
function Message() {
    // JSX : javascript xml
    return <h1>გამარჯობა !</h1>    
}

export default Message;
                

როგორც ვხედავთ კომპონენტის დასახელება ჩავწერეთ ე.წ PascallCase სტილით, რომლის მიხედვითაც ყოველი სიტყვის პირველი ასო იწერება მაღალი რეგისტრით, ეს არის ყველაზე გავრცელებული და მიღებული მიდგომა კომპონენტების სახელდებისას.

რა ხდება შემდეგ ? პირდაპირ ჯავასკრიპტში ჩავწერეთ HTML კოდი ? 🙄 დიახ, ასეა.

რა არის JSX ?

JSX (JavaScript XML) არის ECMAScript-ის სინტაქსური გაფართოება, რომლის მეშვეობითაც შესაძლებელია ჯავასკრიპტში ვწეროთ HTML კოდი. შესრულებამდე, JSX კოდი სტანდარტული ჯავასკრიპტის კოდად გარდაიქმნება :

const element = <h1>გამარჯობა!</h1>;
                

შედეგი იქნება :

const element = React.createElement('h1', null, 'გამარჯობა!');
                

React.createElement ფუნქციას მეორე პარამეტრად, ობიექტში აღწერილი ატრიბუტები და მათი მნიშვნელობები გადაეცემა ხოლმე. ამ ეტაპზე ეს პარამეტრი არ გვაინტერესებს.

***

დავუბრუნდეთ ჩვენს კომპონენტს :

export default Message;                
                

ეს ჩანაწერი საშუალებას გვაძლევს კომპონენტი ხელმისაწვდომი გავხადოთ ჩვენი პროექტის ნებისმიერ წერტილში (უცხო სიტყვათა ლექს: ექსპორტი - საქონლის გატანა საზღვარგარეთ გასაყიდად). სიტყვაგასაღები default აღნიშნავს რომ /src/Message.tsx ფაილიდან საექსპორტო ნაგულისხმები ელემენტი არის Message ფუნქცია.

ნებისმიერ ჯავასკრიპტ ფაილში შეიძლება გვქონდეს მხოლოდ ერთი ნაგულისხმები საექსპორტო ელემენტი, სხვა სახელდებული ექსპორტების რაოდენობაში შეზღუდულები არ ვართ :

// ფაილი: MyComponent.js

// განვსაზღროთ Message კომპონენტი
function Message() {
    return <h1>Hello!</h1>;
}

// სახელდებული ექსპორტი
export function greet() {
    return 'გამარჯობა!';
}


// მეორე სახელდებული ექსპორტი
export const name = 'React';


// ნაგულისხმები ექსპორტი
export default Message;  
                                                
                
// ფაილი სადაც ვაიმპორტებთ: ImportHere.js
import Message, { greet, name } from './MyComponent'; // ნაგულისხმები და სახელდებული იმპორტები                           
                

შევნიშნოთ რომ ნაგულისხმები ექსპორტების იმპორტისას ფიგურული ფრჩხილები არ გამოიყენება, სახელდებულებისას კი ფრჩხილები საჭიროა.

ვიდრე შექმნილი კომპონენტის გამოყენებას შევუდგებით, /src/Message.tsx ფაილის გაფართოებაზეც ვთქვათ ორიოდ სიტყვა. .tsx გაფართოება მიიღება TypeScript და JSX კომბინაციით. ამ გაფართოების ფაილებს მაშინ იყენებენ ხოლმე, როდესაც ძირითადი ენა ტაიპსკრიპტია და მასში JSX-ის გამოყენებაც უწევთ (სატესტოდ სცადეთ და გაფართოებად მიუთითეთ '.ts').

ახლა გადავინაცვლოთ /src/App.tsx ფაილში, წავშალოთ მასში ყველაფერი და შევიტანოთ ეს კოდი :

import Message from './Message'

function App() {
    return <div><Message></Message></div>
}

export default App;
                

იგივეს გაკეთება შეგვიძლია ასეც :

import Message from './Message'

function App() {
    return <div><Message /></div>
}

export default App;
                

როგორც ვხედავთ, <Message /> კომპონენტი გამოვიყენეთ ჩვეულებრივი HTML ელემენტივით. ნებისმიერ კომპონენტს უნდა გააჩნდეს დამხურავი მეწყვილე, ან დაიხუროს '/' სიმბოლოთი, როგორც ეს მეორე მაგალითშია.

თუ ამ ცვლილებებისას პროექტი უკვე ამუშავებული გვქონდა npm run dev ბრძანებით, ტერმინალში ვიხილავთ ამდაგვარ შეტყობინებებს :


როგორც კი /src/App.tsx ფაილის კოდში ცვლილებები შევიტანეთ და შევინახეთ ფაილი, HMR-მა ეს ცვლილებები დაუყოვნებლივ ასახა ბრაუზერში 😍.

აუცილებლად უნდა აღინიშნოს რომ JSX-ში შესაძლებელია ჯავასკრიპტის ნებისმიერი ჩანაწერის გამოყენება: ცვლადების, პირობითი ოპერატორების, მათემატიკური ოპერაციების და ა.შ :

function Message() {
    const name = 'ვასო';
    return <h1>გამარჯობა {name}!</h1>    
}

export default Message;
                

JSX-ში ჯავასკრიპტის კოდის ჩასმა ხდება ფიგურული ფრჩხილების - '{}' გამოყენებით. ამ საკითხს მომავალშიც არაერთხელ შევეხებით.

მაშ ასე :

  1. შევქმენით ჩვენი პირველი კომპონენტი - Message.
  2. App და Message კომპონენტებისაგან შეიქმნა კომპონენტთა იერარქიული ხე.
  3. შეიქმნა ვირტუალური DOM.
  4. შევიტანეთ ცვლილებები Message კომპონენტში, HMR-მა დააფიქსირა ეს ცვლილებები, შექმნა ახალი ვირტუალური DOM, შეადარა იგი ძველს (რეკონსილაცია, 'diffing'), ნახა რომ ეს ვერსიები განსხვავდებიან და ეს სხვაობა ასახა რეალურ DOM-ში.
  5. შედეგად ბრაუზერშიც ვიხილეთ ახალი შიგთავსი 😎
5. კვლავ კომპონენტები

ვიდრე სხვა კომპონენტებსაც შევქმნიდეთ, ჩვენს პროექტში დავაინსტალიროთ Bootstrap. ამისათვის ტერმინალში გავუშვათ შემდეგი ბრძანება :

npm install bootstrap@5.2.3
                

იგივეს გაკეთება შეგვიძლია ასეც :

npm i bootstrap@5.2.3
                

თუ package.json და package-lock.json ფაილებს დავაკვირდებით, ვნახავთ რომ დამატებული იქნება ახლახანს დაინსტალირებული დამოკიდებულება :


ახლა ხელი შევავლოთ CSS ფაილებს. გავასუფთავოთ /src/App.css ფაილი. /src/index.css ფაილი კი საერთოდ წავშალოთ.

/src/main.tsx ფაილში /src/index.css ფაილის ნაცვლად შევაიმპორტოთ Bootstrap :

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import 'bootstrap/dist/css/bootstrap.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
    <App />
    </React.StrictMode>,
)
                

/src საქაღალდეში შევქმნათ components საქაღალდე, მასში კი ფაილი - ListGroup.tsx :

export function ListGroup() {
    return (
        <ul className="list-group">
            <li className="list-group-item">პირველი</li>
            <li className="list-group-item">მეორე</li>
            <li className="list-group-item">მესამე</li>
            <li className="list-group-item">მეოთხე</li>
        </ul>
    );
}
    
export default ListGroup;
                

შევნიშნოთ რომ კლასების აღსაწერად გამოვიყენეთ 'className' ჩანაწერი და არა 'class'. ეს უკანასკნელი, ჯავასკრიპტის რეზერვირებული სიტყვაა და JSX-ში არსებულ HTML კოდში მისი გამოყენების უფლება არ გვაქვს.

ასევე : 'ul' ელემენტი მრგვალ ფრჩხილებშია მოქცეული - როდესაც JSX-ში მრავალხაზიან HTML კოდს ვწერთ (markup), იგი უნდა მოექცეს მრგვალ ფრჩხილებში.

App.tsx :

import ListGroup from './components/ListGroup';

function App() {
    return <div><ListGroup/></div>
}

export default App;                                    
                

დავუშვათ გვინდა რომ 'ul' ელემენტს წინ დავურთოთ 'h1' :

export function ListGroup() {
    return (
        <h1>სია</h1>
        <ul className="list-group">
            <li className="list-group-item">პირველი</li>
            <li className="list-group-item">მეორე</li>
            <li className="list-group-item">მესამე</li>
            <li className="list-group-item">მეოთხე</li>
        </ul>
    );
}
    
export default ListGroup;
                

დაფიქსირდება შემდეგი შეცდომა :

Adjacent JSX elements must be wrapped in an enclosing tag
                

ეს იმიტომ რომ React კომპონენტში შესაძლებელია მხოლოდ ერთი მშობელი ელემენტის დაბრუნება, ყველაფერი უნდა მოექცეს ერთ საერთო მშობელ ელემენტში. არსებობს ამ პრობლემის მოგვარების რამდენიმე ვარიანტი. ერთი - მოვაქციოთ ეს ყოველივე მაგალითად 'div' ელემენტში :

export function ListGroup() {
    return (
        <div>
            <h1>სია</h1>
            <ul className="list-group">
                <li className="list-group-item">პირველი</li>
                <li className="list-group-item">მეორე</li>
                <li className="list-group-item">მესამე</li>
                <li className="list-group-item">მეოთხე</li>
            </ul>
        </div>        
    );
}
    
export default ListGroup;
                

თუმცა ზედმეტი ელემენტის დამატება არც ისე კარგი აზრია, არსებოს უკეთესი გზა - ფრაგმენტი.

რა არის ფრაგმენტი ?

რეაქტში ფრაგმენტი არის გარსაკრი ელემენტი, რომელიც საშუალებას გვაძლევს დავაჯგუფოთ შვილობილი ელემენტები ზედმეტი HTML ელემენტის შექმნის და შესაბამისად - DOM-ში ახალი კვანძის დამატების გარეშე.

import { Fragment } from "react/jsx-runtime";

export function ListGroup() {
    return (
        <Fragment>
            <h1>სია</h1>
            <ul className="list-group">
                <li className="list-group-item">პირველი</li>
                <li className="list-group-item">მეორე</li>
                <li className="list-group-item">მესამე</li>
                <li className="list-group-item">მეოთხე</li>
            </ul>
        </Fragment>        
    );
}
    
export default ListGroup;                                    
                

არსებობს უკეთესი ვარიანტიც :

export function ListGroup() {
    return (
        <>
            <h1>სია</h1>
            <ul className="list-group">
                <li className="list-group-item">პირველი</li>
                <li className="list-group-item">მეორე</li>
                <li className="list-group-item">მესამე</li>
                <li className="list-group-item">მეოთხე</li>
            </ul>
        </>       
    );
}
    
export default ListGroup;                                                                  
                

როდესაც '<>...</>' ჩანაწერს ვიყენებთ, რეაქტი ხვდება რომ ეს არის ფრაგმენტის შემცვლელი, შემოკლებული ვარიანტი და მას ფრაგმენტად აღიქვამს.

ოდნავ გავადინამიკუროთ ჩვენი სია :

export function ListGroup() {
    const cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
    return (
        <>
            <h1>სია</h1>
            <ul className="list-group">
                {cities.map((city) => (
                    <li className="list-group-item">{city}</li>
                ))}
            </ul>
        </>
    );
}
    
export default ListGroup;                                                                                
                
დასაწყისში აღვნიშნეთ, რომ რეაქტის შესწავლის მსურველებს გარკვეული წარმოდგენა უნდა ჰქონდეთ რიგ საკითხებზე და მათ შორის ჯავასკრიპტზეც. მაგალითად, ახლა ჩვენ გამოვიყენეთ მასივებთან სამუშაო 'map' ფუნქცია და ვგულისხმობთ რომ გვესმის და ვიცით თუ როგორ მუშაობს იგი. ასეთი საკითხების განმარტებებზე დროს არ დავხარჯავთ ხოლმე.

თუ ახლა კონსოლს გადავამოწმებთ, ვიხილავთ ასეთ შეტყობინებას :

Warning: Each child in a list should have a unique "key" prop.                                                         
                

როდესაც 'map' მეთოდის დახმარებით ერთი და იგივე ტიპის რამდენიმე ელემენტს ვაგენერირებთ (ჩვენს შემთხვევაში li), რეაქტს თითოეული ამ ელემენტისათვის ესაჭიროება რაიმე ატრიბუტი უნიკალური მნიშვნელობებით რათა საჭიროების მიხედვით მისწვდეს თითოეულ მათგანს : წაშალოს, დაარედაქტიროს იგი და ა.შ.

export function ListGroup() {
    const cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
    return (
        <>
            <h1>სია</h1>
            <ul className="list-group">
                {cities.map((city) => (
                    <li className="list-group-item" key={city}>{city}</li>
                ))}
            </ul>
        </>
    );
}
    
export default ListGroup;                                                                                
                

როგორც წესი, ამ ატრიბუტის მნიშვნელობად, მონაცემთა ბაზიდან წამოღებული ჩანაწერების იდენტიფიკატორები (id) ეთითება ხოლმე, ამ ეტაპზე კი ამით დავკმაყოფილდეთ.

ელემენტთა გენერირება პირობითი ოპერატორების მიხედვით

ხანდახან საჭიროა რომ შიგთავსი (content) რაიმე პირობიდან გამომდინარე დაგენერირდეს :

export function ListGroup() {
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
    cities = []; //  გავასუფთავოთ მასივი

    if (cities.length === 0)
        return (
        <>
            <h1>სია</h1>
            <p>სია ცარიელია</p>
        </>
        );

    return (
        <>
        <h1>სია</h1>
        <ul className="list-group">
            {cities.map((city) => (
            <li className="list-group-item" key={city}>
                {city}
            </li>
            ))}
        </ul>
        </>
    );
}

export default ListGroup;                                                                                           
                

იგივეს უფრო მოკლედ ჩაწერა ტერნარული ოპერატორით შეგვიძლია :

export function ListGroup() {
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
    cities = []; //  გავასუფთავოთ მასივი

    return (
        <>
        <h1>სია</h1>
        {cities.length === 0 ? <p>სია ცარიელია</p> : null}
        <ul className="list-group">
            {cities.map((city) => (
            <li className="list-group-item" key={city}>
                {city}
            </li>
            ))}
        </ul>
        </>
    );
}

export default ListGroup;                                                                                                    
                

კიდევ უფრო დავხვეწოთ კოდი :

export function ListGroup() {
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
    cities = []; //  გავასუფთავოთ მასივი
    const message = cities.length === 0 ? <p>სია ცარიელია</p> : null

    return (
        <>
        <h1>სია</h1>
        {message}
        <ul className="list-group">
            {cities.map((city) => (
            <li className="list-group-item" key={city}>
                {city}
            </li>
            ))}
        </ul>
        </>
    );
}

export default ListGroup;                                                                                                   
                

შეგვიძლია გამოვიყენოთ ფუნქციაც :

export function ListGroup() {
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
    cities = []; //  გავასუფთავოთ მასივი

    const getMessage = () => {
        return cities.length === 0 ? <p>სია ცარიელია</p> : null;
    }

    return (
        <>
        <h1>სია</h1>
        {getMessage()}
        <ul className="list-group">
            {cities.map((city) => (
            <li className="list-group-item" key={city}>
                {city}
            </li>
            ))}
        </ul>
        </>
    );
}

export default ListGroup;                                                                                                          
                

დავუბრუნდეთ ისევ წინანდელ კოდს :

export function ListGroup() {
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
    cities = []; //  გავასუფთავოთ მასივი

    return (
        <>
        <h1>სია</h1>
        {cities.length === 0 ? <p>სია ცარიელია</p> : null}
        <ul className="list-group">
            {cities.map((city) => (
            <li className="list-group-item" key={city}>
                {city}
            </li>
            ))}
        </ul>
        </>
    );
}

export default ListGroup;                                                                                                    
                

ამგვარ შემთხვევებში, ყველაზე ხშირად ლოგიკურ 'და'-ს, იგივე '&&'-ს იყენებენ ხოლმე :

export function ListGroup() {
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
    cities = []; //  გავასუფთავოთ მასივი

    return (
        <>
        <h1>სია</h1>
        {cities.length === 0 && <p>სია ცარიელია</p>}
        <ul className="list-group">
            {cities.map((city) => (
            <li className="list-group-item" key={city}>
                {city}
            </li>
            ))}
        </ul>
        </>
    );
}

export default ListGroup;                                                                                                    
                

ახლა ვნახოთ როგორ მუშაობს ეს ჩანაწერი. განვიხილოთ შემდეგი კოდი :

let logic1 = true && 'ვასო';
console.log(logic1); // ვასო

let logic2 = 5 && 'react';
console.log(logic2); // react

let logic3 = 0 && 'ვასო';
console.log(logic3); // 0

let logic4 = false && 'react';
console.log(logic4); // false                                                                           
                

როდესაც ლოგიკურ 'და'-ს ვიყენებთ, თუ მისი პირველი ოპერანდის მნიშვნელობა ჭეშმარიტია, ანუ true, მაშინ შედეგად ბრუნდება მისი მეორე ოპერანდის მნიშვნელობა, წინააღმდეგ შემთხვევაში კი პირველი ოპერანდი, ანუ ჭეშმარიტების თვალსაზრისით false. შესაბამისად, ამ ჩანაწერშიც :

{cities.length === 0 && <p>სია ცარიელია</p>}                                                           
                

თუ პირველი ოპერანდის მნიშვნელობა ჭეშმარიტია, ანუ მასივის სიგრძე 0-ის ტოლია, ანუ მასივი ცარიელია, მაშინ დაბრუნდება მეორე ოპერანდი ანუ '<p>სია ცარიელია</p>', წინააღმდეგ შემთხვევაში - არაფერი, false.

6. მოვლენათა დამუშავება

ჯავასკრიპტის მოვლენების შესახებ დაწვრილებითი ინფორმაცია შეგიძლიათ იხილოთ ჯავასკრიპტის კურსის 25-ე თავში.

ჩვენს 'li' ელემენტებს მივამაგროთ მაუსის დაჭერის მოვლენა. ამ მოვლენის დასამუშავებლად რეაქტში გამოიყენება ელემენტთა თვისება onClick, რომელსაც პარამეტრად გადაეცემა ის ქმედება ანუ ფუნქცია, რომელიც გვსურს რომ შესრულდეს ელემენტზე მაუსის დაჭერისას. დასაწყისისთვის გადავცეთ მარტივი ისარ-ფუნქცია:

export function ListGroup() {
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
    
    return (
        <>
        <h1>სია</h1>
        {cities.length === 0 && <p>სია ცარიელია</p>}
        <ul className="list-group">
            {cities.map((city, index) => (
            <li
                className="list-group-item"
                key={city}
                onClick={() => console.log(city, index)}
            >
                {city}
            </li>
            ))}
        </ul>
        </>
    );
}

export default ListGroup;                     
                

შეგვიძლია ისარ-ფუნქციას არგუმენტად გადავცეთ რაიმე ცვლადი, რომელშიც ავტომატურად შეინახება ბრაუზერის მოვლენის ობიექტი (ამ შემთხვევაში მაუსის დაჭერის) :

export function ListGroup() {
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
    
    return (
        <>
        <h1>სია</h1>
        {cities.length === 0 && <p>სია ცარიელია</p>}
        <ul className="list-group">
            {cities.map((city) => (
            <li
                className="list-group-item"
                key={city}
                onClick={(event) => console.log(event)}
            >
                {city}
            </li>
            ))}
        </ul>
        </>
    );
}

export default ListGroup;                     
                

რა თქმა უნდა, უმჯობესია თუ ისარ-ფუნქციას ცალკე აღვწერთ :

import { MouseEvent } from "react";

export function ListGroup() {
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];

    // მოვლენის დამმუშავებელი
    const handleClick = (event: MouseEvent) => {
        console.log(event);
    }

    return (
    <>
        <h1>სია</h1>
        {cities.length === 0 && <p>სია ცარიელია</p>}
        <ul className="list-group">
        {cities.map((city) => (
            <li
             className="list-group-item"
             key={city}
             onClick={handleClick}
            >
            {city}
            </li>
        ))}
        </ul>
    </>
    );
}

export default ListGroup;
                                  
                

შევნიშნოთ რომ დაგვჭირდა MouseEvent ტიპის იმპორტი, რათა ტაიპსკრიპტს, event ცვლადის ტიპის დაუზუსტებლობის შეცდომა არ დაეფიქსირებინა.

ასევე შევნიშნოთ რომ handleClick ფუნქცია ფრჩხილების გარეშე მივამაგრეთ ელემენტს : onClick={handleClick}.

7. კომპონენტის მდგომარეობა (state), რა არის ჰუკი (hook) ? useState ჰუკი

რეაქტში, state ანუ მდგომარეობა, არის უმნიშვნელოვანესი კონცეფცია, რომელიც გამოიყენება კომპონენტებში დინამიკური ინფორმაციის მეთვალყურეობისა და მართვისათვის. state-ში ინახება კომპონენტის კონკრეტული მდგომარეობა. როდესაც მომხმარებელი ასრულებს რაიმე ქმედებას, როგორც წესი, საჭიროა ხოლმე სამომხმარებლო ინტერფეისის განახლება ამ ქმედებიდან გამომდინარე.

მაგალითად : ჩვენი სია ამ კონკრეტულ მომენტში გამოიყურება ასე :




როდესაც მომხმარებელი დააწვება რომელიმე ქალაქს, გამოვყოთ ეს ქალაქი სიაში, ანუ შევცვალოთ კომპონენტის მდგომარეობა. ერთი შეხედვით, ეს შესაძლებელია ListGroup.tsx ფაილში საკმაოდ მარტივი ცვლილებების შეტანით :

export function ListGroup() {
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
    let selectedIndex = -1
    
    return (
        <>
        <h1>სია</h1>
        {cities.length === 0 && <p>სია ცარიელია</p>}
        <ul className="list-group">
            {cities.map((city, index) => (
            <li
            className={
                index === selectedIndex
                    ? "list-group-item active"
                    : "list-group-item"
                }
                key={city}
                onClick={() => {selectedIndex = index}}
            >
                {city}
            </li>
            ))}
        </ul>
        </>
    );
}
    
export default ListGroup;                      
                

ეს კოდი არ იმუშავებს და აი რატომ : selectedIndex ცვლადი არის ListGroup კომპონენტში აღწერილი ლოკალური ცვლადი და აპლიკაციის დანარჩენმა ნაწილმა მის შესახებ არაფერი იცის, ამიტომ, რამენაირად მთლიან აპლიკაციას უნდა გავაგებინოთ რომ კომპონენტი შეიცავს დინამიკურ ინფორმაციას - li ელემენტის კლასს, რომელიც შესაძლებელია შეიცვალოს მომხმარებლის ქმედებიდან გამომდინარე, რასაც კომპონენტის მდგომარეობის განახლება უნდა მოჰყვეს. ამისათვის გამოიყენება რეაქტში არსებული ჰუკ-ფუნქცია useState.

რა არის ჰუკი ?

რეაქტში ჰუკი (hook) ეწოდება დამხმარე-ფუნქციას, რომელიც საშუალებას გვაძლევს ფუნქციაზე დაფუძნებულ კომპონენტებშიც გვქონდეს წვდომა კომპონენტების მდგომარეობასა (state) და სასიცოცხლო ციკლებთან (lifecycle). ჰუკების დამატება რეაქტის 16.8 ვერსიაში მოხდა, რომლის გამოსვლამდეც, აღნიშნულ საკითხებთან მუშაობა მხოლოდ კლასზე დაფუძნებულ კომპონენტებში იყო შესაძლებელი.

ჰუკის გამოძახება შეიძლება მხოლოდ ფუნქციაზე დაფუძნებულ კომპონენტებსა და ჩვენს მიერ შექმნილ - სამომხმარებლო ჰუკებში, თანაც მხოლოდ ციკლების, პირობითი ოპერატორების და სხვა დამხარე ფუნქციების გარეთ, ანუ კომპონენტების ან სამომხმარებლო ჰუკების ძირითად ტანში.

ჰუკების მეშვეობით გაცილებით მარტივი და ლამაზი კოდი იწერება. მაგალითად, ზემოთ მოყვანილი ამოცანა კლასზე დაფუძნებული კომპონენტით გადაიჭრებოდა ასე - ListGroup.tsx :

import React, { Component } from "react";

class ListGroup extends Component {
    constructor(props) {
        super(props);
        this.state = {
            selectedIndex: -1,
        };
    }

    handleClick(index) {
        this.setState({ selectedIndex: index });
    }

    render() {
        const cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
        const { selectedIndex } = this.state;

        return (
            <>
            <h1>სია</h1>
            {cities.length === 0 && <p>სია ცარიელია</p>}
            <ul className="list-group">
                {cities.map((city, index) => (
                <li
                    key={city}
                    className={
                    index === selectedIndex
                        ? "list-group-item active"
                        : "list-group-item"
                    }
                    onClick={() => this.handleClick(index)}
                >
                    {city}
                </li>
                ))}
            </ul>
            </>
        );
    }
}

export default ListGroup;                    
                

ფუნქციაზე დაფუძნებული კომპონენტით კი ასე :

import { useState } from "react";

export function ListGroup() {
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
    const [selectedIndex, setSelectedIndex] = useState(-1);

    return (
    <>
        <h1>სია</h1>
        {cities.length === 0 && <p>სია ცარიელია</p>}
        <ul className="list-group">
        {cities.map((city, index) => (
            <li
            className={
                index === selectedIndex
                ? "list-group-item active"
                : "list-group-item"
            }
            key={city}
            onClick={() => {
                setSelectedIndex(index);
            }}
            >
            {city}
            </li>
        ))}
        </ul>
    </>
    );
}

export default ListGroup;           
                

განვიხილოთ ეს კოდი:

1. selectedIndex ცვლადი არის კომპონენტის მდგომარეობის ნაწილი, რომელიც თავის თავში ინახავს არჩეული li ელემენტის, ანუ არჩეული ქალაქის ინდექსს. დასაწყისში ცვლადის მნიშვნელობაა -1, რაც იმას ნიშნავს რომ კომპონენტის თავდაპირველი ჩატვირთვისას არცერთი ქალაქია არჩეული.

2. useState ჰუკში ვიყენებთ setSelectedIndex ფუნქციას, რომელიც გამოიყენება selectedIndex მდგომარეობის განახლებისათვის. როდესაც ამ ფუნქციას ვიძახებთ, რეაქტი ხვდება რომ საჭიროა კომპონენტის ხელახალი გენერირება selectedIndex ცვლადის ახალი მნიშვნელობის მიხედვით.

3. .map() ფუნქციაში selectedIndex ცვლადის მნიშვნელობა შედარებულია თითოეული ქალაქის ინდექსთან, ამით სისტემა ხვდება თუ რომელი ქალაქი უნდა გაააქტიუროს.

4. როდესაც რომელიმე li ელემენტს ვაწვებით, onClick დამმუშავებელი იძახებს setSelectedIndex(index) ფუნქციას. ამის შედეგად ხდება არჩეული ქალაქის ინდექსის, ანუ selectedIndex ცვლადის განახლება, რეაქტი კი ახდენს კომპონენტის ხელახალ გენერირებას.

ამ ყველაფრის შედეგად, თუ მაუსს დავაჭერთ ქალაქ მცხეთას, შეიცვლება კომპონენტის მდგომარეობა და მივიღებთ შემდეგ სურათს :




აუცილებლად უნდა აღინიშნოს რომ თითოეულ კომპონენტს, გამოძახების თითოეულ ადგილას გააჩნია ცალკე, საკუთარი მდგომარეობა (state). App.tsx ფაილში სია გამოვიძახოთ ორჯერ :

import ListGroup from "./components/ListGroup";

function App() {
    return (
    <div>
        <ListGroup />
        <ListGroup />
    </div>
    );
}

export default App;                    
                

თუ პირველ სიაში ავირჩევთ მცხეთას, ხოლო მეორეში ბათუმს, შედეგი იქნება :



მუშაობა ობიექტებთან

კარგი პრაქტიკაა თუ ერთმანეთთან დაკავშირებულ მდგომარეობებს ერთ სივრცეში მოვაქცევთ ხომე, მაგალითად ობიექტებში. დავუშვათ კომპონენტში გვაქვს რომელიმე სასმელის დასახელებისა და ფასის მდგომარეობები, ასევე გვაქვს ღილაკი, რომელზე დაჭერისასაც უნდა შევცვალოთ სასმელის ფასი.

ასეთ შემთხვევაში პირდაპირ ობიექტის ველზე მიმართვა არ იმუშავებს :

import { useState } from "react";

function App() {
    const [drink, setDrink] = useState({
        title: "ყავა",
        price: 5,
    });

    const handleClick = () => {
        drink.price = 6;
        setDrink(drink);
    };

    return (
        <div>
            {drink.price}
            <button onClick={handleClick}>ფასის ცვლილება</button>
        </div>
    );
}

export default App;               
                

იმისათვის, რათა ობიექტში აღწერილი მდგომარეობები შევცვალოთ, საჭიროა შევქმნათ ახალი ობიექტი და მისი მეშვეობით მოვახდინოთ მდგომარეობის ცვლილება :

import { useState } from "react";

function App() {
    const [drink, setDrink] = useState({
        title: "ყავა",
        price: 5,
    });

    const handleClick = () => {
        const newDrink = {
          title: drink.title,
          price: 6
        }
        setDrink(newDrink);
    };

    return (
        <div>
            {drink.price}
            <button onClick={handleClick}>ფასის ცვლილება</button>
        </div>
    );
}

export default App;               
                

მოყვანილ მაგალითში საკმაოდ მარტივი ობიექტი განვიხილეთ, თუმცა შეიძლება მოხდეს ისე, რომ ობიექტს ძალიან ბევრი ველები გააჩნდეს. ასეთ შემთხვევაში, ჯავასკრიპტში არსებულ '...' ოპერატორს იყენებენ ხოლმე, იგი დააკოპირებს უკვე არსებული drink ობიექტის ყველა ველს და შეცვლის მხოლოდ იმას, რასაც ხელახა განვსაზღვრავთ (ამ შემთხვევაში ფასს) :

import { useState } from "react";

function App() {
    const [drink, setDrink] = useState({
        title: "ყავა",
        price: 5,
    });

    const handleClick = () => {
        const newDrink = {
            ...drink,
            price: 6
        }
        setDrink(newDrink);
    };

    return (
        <div>
            {drink.price}
            <button onClick={handleClick}>ფასის ცვლილება</button>
        </div>
    );
}

export default App;               
                

იგივეს ჩაწერა უფრო მოკლედაც შეიძლება :

import { useState } from "react";

function App() {
    const [drink, setDrink] = useState({
        title: "ყავა",
        price: 5,
    });

    const handleClick = () => {
        setDrink({...drink, price: 6});
    };

    return (
        <div>
            {drink.price}
            <button onClick={handleClick}>ფასის ცვლილება</button>
        </div>
    );
}

export default App;               
                

მუშაობა ჩადგმულ ობიექტებთან

ცოტა უფრო ჩავხლართოთ ჩვენი ობიექტი 😀 :

import { useState } from "react";

function App() {
    const [drink, setDrink] = useState({
        title: "ყავა",
        price: 5,
        brand: {
            title: "ნესკაფე",
            code: 123456,
        },
    });

    const handleClick = () => {
        setDrink({...drink, brand: { ...drink.brand, code: 789021 } });
    };

    return (
        <div>
            {drink.brand.code}
            <button onClick={handleClick}>კოდის ცვლილება</button>
        </div>
    );
}

export default App;                      
                

ზოგადად, მდგომარეობის ობიექტების ძალიან ჩახლართვა არც ისე კარგი პრაქტიკაა, ჩვენ უბრალოდ სადემონსტრაციოდ მოვიყვანეთ მაგალითი, თუ როგორ შეიძლება ჩადგმული ობიექტის ველებთან წვდომა.

მუშაობა მასივებთან

ვფიქრობ ამ კოდში განსაკუთრებული არაფერი ხდება, ამიტომ პირდაპირ მაგალითი მოვიყვანოთ :

import { useState } from "react";

function App() {
    const [colors, setColors] = useState(["წითელი", "ყვითელი", "მწვანე"]);

    const handleClick = () => {
        // დამატება
        setColors([...colors, "ლურჯი"]);

        // წაშლა
        setColors(colors.filter((color) => color != "წითელი"));

        // განახლება
        setColors(
            colors.map((color) => (color === "წითელი" ? "იასამნისფერი" : color))
        );
    };

    return (
        <div>
            <button onClick={handleClick}>ფერების ცვლილება</button>
        </div>
    );
}

export default App;                                        
                

დავაბრუნოთ ListGroup კომპონენტი App.tsx ფაილში :

import ListGroup from "./components/ListGroup";

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

export default App;   
                
8. კომპონენტის რეკვიზიტები, props

აღწერილ სიაში ქალაქების დასახელებები გვაქვს. შეიძლება მოხდეს ისე, რომ დაგვჭირდეს იგივე სტრუქტურის მქონე სხვა სია, მაგალითად ფერთა დასახელებების სია. ამ დროს ListGroup.tsx ფაილის ასლის შექმნა ცუდი აზრია. უმჯობესია თუ ამ ფაილს მრავალჯერადად გამოყენებადს გავხდით. ამისათვის გამოვიყენოთ რეაქტში არსებული props მექანიზმი, ანუ რეკვიზიტების სისტემა, რომელიც არის კომპონენტებს შორის ინფორმაციის გაცვლის საშუალება.

სიის დასაგენერირებლად გვჭირდება ორი რამ : სიის სათაური და უშუალოდ ჩამონათვალი. შევქმნათ ინტერფეისი, რომელიც აღწერს ამ სტრუქტურას, შემდეგ კი განვსაზღვროთ ListGroup კომპონენტის რეკვიზიტი :

import { useState } from "react";

interface ListGroupProps {
    heading : string, // სიის სათაური
    items: string[] // სტრიქონების მასივი, სიაში გამოსატანი ინფორმაცია
}

export function ListGroup(listProps: ListGroupProps) {
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];
    const [selectedIndex, setSelectedIndex] = useState(-1);

    return (
    <>
        <h1>სია</h1>
        {cities.length === 0 && <p>სია ცარიელია</p>}
        <ul className="list-group">
        {cities.map((city, index) => (
            <li
            className={
                index === selectedIndex
                ? "list-group-item active"
                : "list-group-item"
            }
            key={city}
            onClick={() => {
                setSelectedIndex(index);
            }}
            >
            {city}
            </li>
        ))}
        </ul>
    </>
    );
}

export default ListGroup;                            
                

როგორც ვხედავთ, ListGroup კომპონენტს, ფუნქციის არგუმენტის მსგავსად გადაეცა listProps რეკვიზიტი, რომელიც არის ListGroupProps ინტერფეისის ტიპის. ეს იმას ნიშნავს, რომ თუ სადმე ამ კომპონენტს გამოვიძახებთ, მას აუცილებლად უნდა გადავცეთ ListGroupProps ინტერფეისში აღწერილი მონაცემების შესაბამისი რეკვიზიტები : სტრიქონული ტიპის 'heading' რეკვიზიტი და სტრიქონების მასივის ტიპის 'items' რეკვიზიტი.

თუ ახლა App.tsx ფაილს ვნახავთ, ტაიპსკრიპტი დაგვახვედრებს ამ შეცდომას :

Type '{}' is missing the following properties from type 'ListGroupProps': heading, items
                

ტაიპსკრიპტი გვაფრთხილებს რომ, როდესაც ListGroup კომპონენტი აღვწერეთ, მას განვუსაზღვრეთ სავალდებულო რეკვიზიტები, App.tsx ფაილში კი ამ რეკვიზიტების გარეშე ვიძახებთ კომპონენტს (არსებობს არასავალდებულო რეკვიზიტებიც და მათ მოგვიანებით განვიხილავთ. ასევე ვნახავთ თუ როგორ ხდება არასავალდებულო რეკვიზიტების განსაზღვრა).

მივამაგროთ რეკვიზიტები კომპონენტს, App.tsx :

import ListGroup from "./components/ListGroup";

function App() {
    let heading = "ქალაქები";
    let items = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];

    return (
        <ListGroup heading={heading} items={items} />
    );
}

export default App;                   
                

როგორც ვხედავთ, კომპონენტზე რეკვიზიტების მიმაგრება HTML ატრიბუტების მსგავსად ხდება.

ახლა გამოვიყენოთ მიმაგრებული რეკვიზიტები, ListGroup.tsx :

import { useState } from "react";

interface ListGroupProps {
    heading : string, // სიის სათაური
    items: string[] // სტრიქონების მასივი, სიაში გამოსატანი ინფორმაცია
}

export function ListGroup(listProps: ListGroupProps) {
    
    const [selectedIndex, setSelectedIndex] = useState(-1);

    return (
    <>
        <h1>{listProps.heading}</h1>
        {listProps.items.length === 0 && <p>სია ცარიელია</p>}
        <ul className="list-group">
        {listProps.items.map((item, index) => (
            <li
            className={
                index === selectedIndex
                ? "list-group-item active"
                : "list-group-item"
            }
            key={item}
            onClick={() => {
                setSelectedIndex(index);
            }}
            >
            {item}
            </li>
        ))}
        </ul>
    </>
    );
}

export default ListGroup;            
                

იგივეს გაკეთება ოდნავ უფრო ლამაზადაც შეგვიძლია :

import { useState } from "react";

interface ListGroupProps {
    heading : string, // სიის სათაური
    items: string[] // სტრიქონების მასივი, სიაში გამოსატანი ინფორმაცია
}

export function ListGroup({heading, items}: ListGroupProps) {
    
    const [selectedIndex, setSelectedIndex] = useState(-1);

    return (
    <>
        <h1>{heading}</h1>
        {items.length === 0 && <p>სია ცარიელია</p>}
        <ul className="list-group">
        {items.map((item, index) => (
            <li
            className={
                index === selectedIndex
                ? "list-group-item active"
                : "list-group-item"
            }
            key={item}
            onClick={() => {
                setSelectedIndex(index);
            }}
            >
            {item}
            </li>
        ))}
        </ul>
    </>
    );
}

export default ListGroup;            
                

თუ ყველაფერს შევაჯამებთ, შეგვიძლია ვთქვათ რომ ListGroup.tsx კომპონენტში აღვწერეთ სიის ზოგადი სტრუქტურა. ახლა, სადაც დაგვჭირდება იქ გამოვიყენებთ ამ სტრუქტურას, გადავცემთ სასურველ ინფორმაციას ანუ რეკვიზიტებს და დავაგენერირებთ შესაბამის სიას, App.tsx :

import ListGroup from "./components/ListGroup";

function App() {
    let citiesHeading = "ქალაქები";
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];

    let colorsHeading = "ფერები";
    let colors = ["წითელი","ყვითელი",'მწვანე'];

    return (
        <>
            <ListGroup heading={citiesHeading} items={cities} />
            <ListGroup heading={colorsHeading} items={colors} />
        </>
    );
}

export default App; 
                

სწორედ ამას ნიშნავს კომპონენტის მრავალგამოყენებადობა (reusability), რაც საკმაოდ მნიშვნელოვანი და ხელსაყრელი რამაა რეაქტში.

ფუნქციების მიმაგრება რეკვიზიტებზე

ჩვენი სიის რომელიმე ელემენტზე მაუსის დაჭერისას ამ ელემენტს უბრალოდ ფონის ფერი ეცვლება. რეალურ პროექტებში, როდესაც მაუსს ვაჭერთ რაიმეს, როგორც წესი, რაღაც ხდება - ან რაიმე ჩამონათვალი იფილტრება, ან რაიმე შეტყობინება გამოდის ეკრანზე და ა.შ.

ListGroup კომპონენტში აღვწეროთ მექანიზმი, რომელიც დააფიქსირებს მაუსის დაჭერის მომენტს, თავისთავად იმასაც, თუ რომელ ელემენტს დაეჭირება მაუსი, შემდეგ კი საჭირო ადგილას გააგზავნის სიგნალს : 'ამა და ამ ელემენტს დაეჭირა მაუსი, გააკეთე რისი გაკეთებაც გინდა, შეასრულე სასურველი ფუნქცია'.

როგორც წესი, სასურველი ფუნცქციის აღწერა მშობელ კომპონენტში ხდება ხოლმე. შვილობილ კომპონენტში ვაფიქსირებთ სასურველ მომენტს (ამ შემთხვევაში სიის ელემენტზე მაუსის დაჭერის მომენტს), შემდეგ კი კომპონენტის გამოძახების ყველა სხვადასხვა ადგილას, ანუ ყველა სხვადასხვა მშობელ კომპონენტში, ან ერთი და იგივეში, მაგრამ რამოდენიმეგან, ვასრულებთ სხვადასხვა ფუნქციას - ერთგან შეტყობინება გამოგვაქვს, მეორეგან რაიმეს ვფილტრავთ, მესამეგან სადმე ვამისამართებთ მომხმარებელს და ა.შ. მოკლედ - სრული თავისუფლება გვაქვს 😌

ListGroup.tsx :

import { useState } from "react";

interface ListGroupProps {
    heading: string; // სიის სათაური
    items: string[]; // სტრიქონების მასივი, სიაში გამოსატანი ინფორმაცია
    onSelectItem: (item: string) => void; // ქალაქის არჩევის მომენტი
}

export function ListGroup({ heading, items, onSelectItem }: ListGroupProps) {
    const [selectedIndex, setSelectedIndex] = useState(-1);

    return (
    <>
        <h1>{heading}</h1>
        {items.length === 0 && <p>სია ცარიელია</p>}
        <ul className="list-group">
        {items.map((item, index) => (
            <li
            className={
                index === selectedIndex
                ? "list-group-item active"
                : "list-group-item"
            }
            key={item}
            onClick={() => {
                setSelectedIndex(index);
                onSelectItem(item);
            }}
            >
            {item}
            </li>
        ))}
        </ul>
    </>
    );
}

export default ListGroup;
                

App.tsx :

import ListGroup from "./components/ListGroup";

function App() {
    let citiesHeading = "ქალაქები";
    let cities = ["თბილისი", "მცხეთა", "ბათუმი", "ქუთაისი"];

    const handleSelectItem = (item: string) => {
        console.log(item);
    };

    return (
        <ListGroup
            heading={citiesHeading}
            items={cities}
            onSelectItem={handleSelectItem}
        />
    );
}

export default App;
                

ჩვენ ახლა App კომპონენტში მივამაგრეთ ფუნქცია ListGroup კომპონენტის რეკვიზიტს, იგივეს გაკეთება შეგვიძლია ნებისმიერ სხვა კომპონენტშიც.

დაბოლოს, შევნიშნოთ რომ თუ ListGroup.tsx ფაილში აღწერილ კოდს ცარიელი მასივი გადაეცემა cities რეკვიზიტად, ანუ თუ App.tsx ფაილში მოვიქცევით ასე :

import ListGroup from "./components/ListGroup";

function App() {
    let citiesHeading = "ქალაქები";
    let cities = [];

    const handleSelectItem = (item: string) => {
        console.log(item);
    };

    return (
        <ListGroup
            heading={citiesHeading}
            items={cities}
            onSelectItem={handleSelectItem}
        />
    );
}

export default App;                    
                

ბრაუზერში მაინც დაგენერირდება <ul className="list-group"></ul> ელემენტი, რომელიც ასევე ცარიელი იქნება. გამოვასწოროთ ეს ტერნარული ოპერატორის მეშვეობით:

import { useState } from "react";

interface ListGroupProps {
    heading: string; // სიის სათაური
    items: string[]; // სტრიქონების მასივი, სიაში გამოსატანი ინფორმაცია
    onSelectItem: (item: string) => void; // ქალაქის არჩევის მომენტი
}

export function ListGroup({ heading, items, onSelectItem }: ListGroupProps) {
    const [selectedIndex, setSelectedIndex] = useState(-1);

    return (
    <>
        <h1>{heading}</h1>
        {items.length === 0 ? (
        <p>სია ცარიელია</p>
        ) : (
        <ul className="list-group">
            {items.map((item, index) => (
            <li
                className={
                index === selectedIndex
                    ? "list-group-item active"
                    : "list-group-item"
                }
                key={item}
                onClick={() => {
                    setSelectedIndex(index);
                    onSelectItem(item);
                }}
            >
                {item}
            </li>
            ))}
        </ul>
        )}
    </>
    );
}

export default ListGroup;                    
                

ისევ დავაბრუნოთ ქალაქები App.tsx ფაილში :

let cities = ['თბილისი', 'მცხეთა', 'ხაშური', 'ბათუმი']; 
                
9. მდგომარეობა VS რეკვიზიტი - state VS props

რეაქტში, მდგომარეობაც (state) და რეკვიზიტიც (props), ინფორმაციის მართვისა და კომპონენტებს შორის გაცვლის ფუნდამენტურ კონცეფციას წარმოადგენს.

მდგომარეობა

  • მდგომარეობის განსაზღვრა ხდება კომპონენტში და იგი პერიოდულად იცვლება მომხმარებლის ქმედებიდან გამომდინარე.
  • კონკრეტული კომპონენტი ფლობს თავის საკუთარ მდგომარეობას და მართავს მას თავადვე. მდგომარეობა წააგავს ლოკალურ ცვლადს, რომელიც კონკრეტულ ფუნქციაშია ხელმისაწვდომი.
  • მდგომარეობის ინიციალიზაცია ხდება useState ჰუკის მეშვეობით, რომელიც თავის თავში მოიცავს მიმდინარე მდგომარეობასა და მდგომარეობის განსაახლებელი ფუნქციის შემცველ მასივს:
    const [selectedIndex, setSelectedIndex] = useState(-1);
                            
  • როდესაც მდგომარეობა იცვლება, ხდება კომპონენტის ხელახალი გენერირება, შესაბამისად იცვლება სამომხმარებლო ინტერფეისის შიგთავსიც.

რეკვიზიტები

  • კომპონენტი რეკვიზიტთა კონკრეტულ მნიშვნელობებს მშობელი კომპონენტიდან ღებულობს და მდგომარეობისაგან განსხვავებით, შვილობოლ კომპონენტში ამ მნიშვნელობათა შეცვლა შეუძლებელია. მაგალითად ListGroup.tsx კომპონენტში ამდაგვარი ჩანაწერის გაკეთება :
    heading = 'შეცვლილი სათაური'
                            
    ქვეყანას არ ჩამოგვაქცევს თავზე 😬, თუმცა ამით heading რეკვიზიტის რეალური მნიშვნელობა, ანუ App.tsx კომპონენტში განსაზღვრული მნიშვნელობა არ შეიცვლება. ეს რეკვიზიტის ხელახალი, ლოკალური ინიციალიზაცია იქნება, რაც არც ისე კარგი პრაქტიკაა და კარგი იქნება თუ ასე არ მოვიქცევით ხოლმე.
  • რეკვიზიტები წააგავნან ფუნქციის არგუმენტებს.
  • რეკვიზიტი ხელს უწყობს კომპონენტთა მრავალგამოყენებადობას : შეგვიძლია ერთი და იგივე კომპონენტს სხვადასხვა რეკვიზიტები გადავცეთ სხვადასხვა ადგილას, რაც სისტემის მოქნილობას უზრუნველყოფს.
  • როდესაც რეკვიზიტის მნიშვნელობა იცვლება, ხდება კომპონენტის ხელახალი გენერირება, შესაბამისად იცვლება სამომხმარებლო ინტერფეისის შიგთავშიც.
10. მუშაობა CSS სტილებთან

სტანდარტული CSS

როგორც ვიცით, CSS სტილების გასაფორმებლად Bootstrap-ს ვიყენებთ. ჩავანაცვლოთ იგი ჩვენი საკუთარი სტილებით. main.tsx ფაილში წავშალოთ შემდეგი ხაზი :

import 'bootstrap/dist/css/bootstrap.css';
                

შევქმნათ ახალი ListGroup.css ფაილი, თუმცა გავითვალისწინოთ შემდეგი რამ : ჩვენ უკვე გვაქვს სიის კომპონენტთან დაკავშირებული ListGroup.tsx ფაილი. ნაცვლად იმისა, რომ ყველაფერი პირდაპირ /src/components საქაღალდეში ჩავყაროთ, უმჯობესია ქვე-საქაღალდეებში დავაჯგუფოთ ერთი და იგივე საკითხთან დაკავშირებული სხვადასხვა ფაილები. ამ შემთხვევაში შევქმნათ /src/components/ListGroup ქვე-საქაღალდე და სიის კომპონენტთან დაკავშირებული ყველა ფაილი განვათავსოთ მასში. ახლადშექმნილ საქაღალდეში შევქმნათ index.ts ფაილიც :



index.ts :

import ListGroup from "./ListGroup";

export default ListGroup;
                

ამ ფაილის გარეშე, App კომპონენტში ListGroup კომპონენტის იმპორტი მოგვიწევდა ასე :

import ListGroup from "./components/ListGroup/ListGroup";
                

რაც არც ისე ლამაზი ჩანაწერია. ახლა App კომპონენტს საერთოდ აღარ შევეხებით, იმპორტი ძველებურადვე დარჩება.

ListGroup.tsx ფაილში ჩავამატოთ სტილების იმპორტი :

import { useState } from "react";
import './ListGroup.css';

...
                

აღვწეროთ რაიმე სტილები ListGroup.css ფაილში :

.list-group {
    list-style: none;
    margin: 0;
    padding: 0;
}

...
                

CSS მოდული

ზემოთ აღწერილი, ანუ სტანდარტული მიდგომით სტილების გაფორმებისას, შეიძლება თავი იჩინოს ასეთმა პრობლემამ : თუ 'list-group' კლასის მქონე ელემენტი სადმე სხვაგანაც გვაქვს, მაშინ მოხდება სტილების ურთიერთგადაწერა. მაგალითად : App.css ფაილში შევიტანოთ შემდეგი კოდი:

.list-group {
    background: red;
}
                

App კომპონენტში კი შევაიმპორტოთ App.css ფაილი :

import './App.css';
                

შედეგი იქნება ამდაგვარი :



ასეთი პრობლემების გადასაჭრელად გამოიყენება CSS მოდულები. CSS მოდულებში ხდება ელემენტთა კლასების ლოკალურ თვალთახედვაში (local scope) აღწერა.

ListGroup.css ფაილს შევუცვალოთ სახელი და დავარქვათ ListGroup.module.css. შევიტანოთ ცვლილებები ListGroup.tsx ფაილში :

import { useState } from "react";
import styles from './ListGroup.module.css';

interface ListGroupProps {
    heading: string; // სიის სათაური
    items: string[]; // სტრიქონების მასივი, სიაში გამოსატანი ინფორმაცია
    onSelectItem: (item: string) => void; // ქალაქის არჩევის მომენტი
}

export function ListGroup({ heading, items, onSelectItem }: ListGroupProps) {
    const [selectedIndex, setSelectedIndex] = useState(-1);

    return (
    <>
        <h1>{heading}</h1>
        {items.length === 0 && <p>სია ცარიელია</p>}
        <ul className={styles['list-group']}>
        {items.map((item, index) => (
            <li
            className={
                index === selectedIndex
                ? "list-group-item active"
                : "list-group-item"
            }
            key={item}
            onClick={() => {
                setSelectedIndex(index);
                onSelectItem(item);
            }}
            >
            {item}
            </li>
        ))}
        </ul>
    </>
    );
}

export default ListGroup;                    
                

მივაქციოთ ყურადღება ამ ჩანაწერს :

import styles from './ListGroup.module.css';
                

როგორც ვხედავთ, სტილების იმპორტი ჯავასკრიპტის ბიბლიოთეკის მსგავსად მოვახდინეთ. დასახელება 'styles' პირობითია და შეგვიძლია დავარქვათ რაც გვსურს. თუ ახლა ჩვენი სიის დეტალებს ვნახავთ, ვიხილავთ ამდაგვარ სურათს :



როდესაც ფაილის დასახელებაში .module.css ურევია, Vite ხვდება რომ CSS მოდულთან აქვს საქმე და ასრულებს შესაბამის სამუშაოებს რათა კლასთა დასახელებები ListGroup კომპონენტის ლოკალურ თვალთახედვის არეში მოაქციოს, კერძოდ - ჰეშავს მათ და ინახავს styles ცვლადში არსებულ ობიექტში:

{
    "list-group": "_list-group_1nso8_1"
}
                

ჰეშირებულ დასახელებებს ვწვდებით როგორც ჩვეულებრივი ობიექტის ველებს :

styles['list-group']
                

სწორედ ეს ჰეშირება განაპირობებს იმას რომ კლასთა კონფლიქტი და შესაბამისად - სტილების ურთიერთგადაწერა არ მოხდება.

მიღებული სტანდარტია კლასთა ე.წ 'camelCase' სტილით ჩაწერა, რომლის მიხედვითაც, პირველი სიტყვის პირველი ასო იწერება დაბალ რეგისტრში, ყოველი შემდეგი სიტყვის პირველი ასო კი - მაღალში :

.listGroup {
    list-style: none;
    margin: 0;
    padding: 0;
}
                

ამ შემთხვევაში კლასებს უფრო ლამაზად მივწვდებით :

...

<ul className={styles.listGroup}>

...
    
</ul>

...
                

ახლა ვნახოთ თუ როგორ ხდება ელემენტისათვის რამდენიმე კლასის ერთდროულად მინიჭება :

.listGroup {
    list-style: none;
    margin: 0;
    padding: 0;
}

.otherClass {
    ...
}
                
...

<ul className={[styles.listGroup, styles.otherClass].join(' ')}>

...
    
</ul>

...
                

ხაზსშიდა სტილები

ხაზსშიდა სტილების (inline styles) წერა არც ისე კარგი პრაქტიკაა, უბრალოდ სადემონსტრაციოდ მოვიყვანოთ მაგალითი თუ როგორ შეიძლება ამის გაკეთება JSX-ში :

...

<ul className={styles.listGroup} style={{ backgroundColor: 'yellow' }}>

...
    
<ul>

...
                

ნებისმიერ ელემენტს გააჩნია style ატრიბუტი, რომელსაც ობიექტის სახით გადაეცემა სასურველი სტილები. ამ ობიექტის აღწერა JSX-ის გარეთაც შესაძლებელია. ისეთი CSS თვისებების აღწერა, რომლებიც ორ და მეტ სიტყვას შეიცავენ, ე.წ 'camelCase' სტილით ხდება.

ხატულები

ხატულებთან (icons) სამუშაოდ react-icons ბიბლიოთეკას გამოვიყენებთ ხოლმე :

npm i react-icons@4.7.1
                

ხელმისაწვდომი ხატულების ნახვა შესაძლებელია ოფიციალურ ვებ-გვერდზე. აქვე იხილავთ გამოყენების ინსტრუქციებსაც.

import { BsFillCalendar2CheckFill } from "react-icons/bs";

<BsFillCalendar2CheckFill size='40' />
                
11. მუშაობა ფორმებთან, useRef ჰუკი

პირველ რიგში, ჩვენს პროექტში დავაბრუნოთ Bootstrap-ი, main.tsx ფაილი :

import 'bootstrap/dist/css/bootstrap.css';
                

შემდეგ შევქმნათ ახალი კომპონენტი - /src/components/Form.tsx :

const Form = () => {
    return (
        <form>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input type="text" className="form-control" id="name" />
            </div>
                <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input type="number" className="form-control" id="age" />
            </div>
            <button className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;                    
                

App.tsx ფაილი :

import Form from "./components/Form";

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

export default App;      
                

ფორმის გაგზავნა

ფორმის გაგზავნის მოვლენის დასაფიქსირებლად რეაქტში გამოიყენება onSubmit დამმუშავებელი :

const Form = () => {
    return (
        <form onSubmit={() => console.log('გაიგზავნა')}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input type="text" className="form-control" id="name" />
            </div>
                <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input type="number" className="form-control" id="age" />
            </div>
            <button className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;                    
                

თუ ახლა გაგზავნის ღილაკს დავაწვებით, გვერდი ხელახლა ჩაიტვირთება, ავირიდოთ ეს თავიდან და ასევე - ფორმის გაგზავნის ფუნქციონალი გავიტანოთ ცალკე ფუნქციაში :

import { FormEvent } from "react";

const Form = () => {
    const handleSubmit = (event: FormEvent) => {
        event.preventDefault();
        console.log("გაიგზავნა");
    };

    return (
        <form onSubmit={handleSubmit}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input type="text" className="form-control" id="name" />
            </div>
            <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input type="number" className="form-control" id="age" />
            </div>
            <button className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;          
                

useRef ჰუკი

რეაქტში useRef ჰუკს აქვს ორი ძირითადი დანიშნულება :

  1. საშუალებას გვაძლევს შევინახოთ ინფორმაცია კომპონენტთა რე-გენერირებებს შორის.
  2. შევძლოთ პირდაპირი წვდომა DOM ელემენტებთან.

ინფორმაციის შენახვა კომპონენტთა რე-გენერირებებს შორის

როგორც ვიცით, useState ჰუკის მეშვეობით კომპონენტში რაიმე ინფორმაციის განახლებისას, ამ კომპონენტის ხელახალი გენერირება (რე-რენდერი) ხდება. დავუშვათ გვაქვს ღილაკი და გვსურს დავთვალოთ თუ რამდენჯერ დაეჭირება მას მაუსი. /src/App.tsx :

import { useState } from "react";

function App() {
    const [clicks, setClicks] = useState(0); 
    let count = 0;

    const handleClick = () => {
        count += 1; 
        console.log("მთვლელი:", count);
        setClicks(clicks + 1); 
    };

    return <button onClick={handleClick}>ღილაკი</button>;
}

export default App;                        
                

ღილაკზე თითოეული დაჭერისას მოხდება შემდეგი :

  1. handleClick დამმუშავებელში გაიზრდება count ცვლადის მნიშვნელობა.
  2. მდგომარეობის განახლება (setClicks) გამოიწვევს კომპონენტის ხელახალ გენერირებას (რე-რენდერი).
  3. count ცვლადის მნიშვნელობა ისევ გახდება 0, რადგანაც მთლიანი ფუნქცია ხელახლა გაეშვება და შესაბამისად ცვლადიც დაუბრუნდება საწყის მდგომარეობას. ანუ მისი მიმდინარე მნიშვნელობა დაიკარგება.

გადავჭრათ ეს ამოცანა useRef ჰუკის მეშვეობით :

import { useRef, useState } from "react";

function App() {
    const [clicks, setClicks] = useState(0); 
    const count = useRef(0);

    const handleClick = () => {
        count.current += 1; 
        console.log("მთვლელი:", count.current);
        setClicks(clicks + 1); 
    };

    return <button onClick={handleClick}>ღილაკი</button>;
}    

export default App;  
                

useRef ჰუკი აბრუნებს ერთადერთ მნიშვნელობას - ობიექტს სახელად current. ჰუკის ინიციალიზაციისას უნდა მოხდეს მისი საწყისი მნიშვნელობის განსაზღვრა. ჩვენს სემთხვევაში - useRef(0). ეს ყოველივე წააგავს შემდეგს :

const count = {current: 0}                         
                

ღილაკზე თითოეული დაჭერისას მოხდება შემდეგი :

  1. handleClick დამმუშავებელში გაიზრდება count.current მნიშვნელობა.
  2. მდგომარეობის განახლება (setClicks) გამოიწვევს ხელახალ გენერირებას (რე-რენდერი).
  3. ხელახალი გენერირების შემდეგაც კი, count ობიექტი ინარჩუნებს თავის ძველ მნიშვნელობას, რადგან useRef არ ნულდება კომპონენტთა გენერირებებს შორის.

აუცილებლად უნდა აღინიშნოს რომ, useState ჰუკისგან განსხვავებით, useRef-ის ცვლილება არ იწვევს კომპონენტის ხელახალ გენერირებას. ამიტომ მისი გამოყენება ხელსაყრელია მაშინ, როდესაც გვჭირდება რაიმე ინფორმაციის კონტროლი და ცვლილება, მაგრამ არ გვინდა ეს ცვლილება სამომხარებლო ინტერფეისში აისახოს.

პირდაპირი წვდომა DOM ელემენტებთან

App.tsx ფაილში დავაბრუნოთ ფორმა :

import Form from "./components/Form";

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

export default App;      
                

იმისათვის, რათა რომელიმე DOM ელემენტთან გვქონდეს პირდაპირი წვდომა, მას უნდა მივამაგროთ ref ატრიბუტი.

ზოგადად, დასახელება useRef მომდინარეობს სიტყვიდან 'reference', რაც ნიშნავს ბმულს, და გამოხატავს ამ ჰუკის ძირითად არსს - გვქონდეს რაიმე ინფორმაციასთან ან ნებისმიერ DOM ელემენტთან კავშირი, წვდომა.

დავუბრუნდეთ ჩვენს თავდაპირველ ფორმას და დავამუშავოთ მასში აკრეფილი ინფორმაცია useRef ჰუკის მეშვეობით, /src/components/Form.tsx :

import { FormEvent, useRef } from "react";

const Form = () => {
    const nameRef = useRef(null); // საწყისი მნიშვნელობა null
    const ageRef = useRef(null); // საწყისი მნიშვნელობა null

    const handleSubmit = (event: FormEvent) => {
        event.preventDefault();
        console.log(nameRef.current.value);
        console.log(ageRef.current.value);
    };

    return (
        <form onSubmit={handleSubmit}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input type="text" className="form-control" id="name" ref={nameRef} />
            </div>
            <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input type="number" className="form-control" id="age" ref={ageRef} />
            </div>
            <button className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;                                                   
                

ედიტორი გამოგვიტანს ამდაგვარ შეცდომებს :

'nameRef.current' is possibly 'null' 
'ageRef.current' is possibly 'null'
                

დავაზღვიოთ ეს შეცდომები :

import { FormEvent, useRef } from "react";

const Form = () => {
    const nameRef = useRef(null); // საწყისი მნიშვნელობა null
    const ageRef = useRef(null); // საწყისი მნიშვნელობა null

    const handleSubmit = (event: FormEvent) => {
        event.preventDefault();
        if (nameRef.current) console.log(nameRef.current.value);
        if (ageRef.current) console.log(ageRef.current.value);
    };

    return (
        <form onSubmit={handleSubmit}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input type="text" className="form-control" id="name" ref={nameRef} />
            </div>
            <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input type="number" className="form-control" id="age" ref={ageRef} />
            </div>
            <button className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;                                                   
                

ახლა ვიხილავთ ამგვარ შეცდომებს :

Property 'value' does not exist on type 'never'
                

ეს იმიტომ ხდება რომ ტაიპსკრიპტის კომპილატორმა არ იცის თუ რა ტიპის ელემენტს მივმართავთ. useRef ჰუკის მეშვეობით ნებისმიერ DOM ელემენტთან წვდომაა შესაძლებელი : p, h1, h2..., მათ კი value ატრიბუტი სულაც არ გააჩნიათ, ამიტომ საჭიროა განისაზღვროს, რომ წვდომა გვსურს input ტიპის ელემენტთან :

import { FormEvent, useRef } from "react";

const Form = () => {
    const nameRef = useRef<HTMLInputElement>(null); // საწყისი მნიშვნელობა null
    const ageRef = useRef<HTMLInputElement>(null); // საწყისი მნიშვნელობა null

    const handleSubmit = (event: FormEvent) => {
        event.preventDefault();
        if (nameRef.current) console.log(nameRef.current.value);
        if (ageRef.current) console.log(ageRef.current.value);
    };

    return (
        <form onSubmit={handleSubmit}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input type="text" className="form-control" id="name" ref={nameRef} />
            </div>
            <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input type="number" className="form-control" id="age" ref={ageRef} />
            </div>
            <button className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;                                                   
                

მივცეთ საბოლოო სახე ფორმას :

import { FormEvent, useRef } from "react";

const Form = () => {
    const nameRef = useRef<HTMLInputElement>(null); // საწყისი მნიშვნელობა null
    const ageRef = useRef<HTMLInputElement>(null); // საწყისი მნიშვნელობა null
    const person = { name: '', age: 0 };

    const handleSubmit = (event: FormEvent) => {
        event.preventDefault();
        if (nameRef.current) person.name = nameRef.current.value;
        if (ageRef.current) person.age = parseInt(ageRef.current.value);
        console.log(person)
    };

    return (
        <form onSubmit={handleSubmit}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input type="text" className="form-control" id="name" ref={nameRef} />
            </div>
            <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input type="number" className="form-control" id="age" ref={ageRef} />
            </div>
            <button className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;                                                        
                

ფორმის დამუშავება useState ჰუკით

ბუნებრივია ფორმის დამუშავება useState ჰუკითაც შეიძლება :

import { FormEvent, useState } from "react";

const Form = () => {
    const [person, setPerson] = useState({ name: "", age: 0 });

    const handleSubmit = (event: FormEvent) => {
        event.preventDefault();
        console.log(person);
    };

    return (
        <form onSubmit={handleSubmit}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input
                    type="text"
                    className="form-control"
                    id="name"
                    onChange={(event) => {
                        setPerson({ ...person, name: event.target.value });
                    }}
                />
            </div>
            <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input
                    type="number"
                    className="form-control"
                    id="age"
                    onChange={(event) => {
                        setPerson({ ...person, age: parseInt(event.target.value) });
                    }}
                />
            </div>
            <button className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;                                                               
                

ასეთი მიდგომისას, რამდენჯერაც ველებში რაიმეს ავკრეფთ, იმდენჯერ მოხდება კომპონენტის რე-გენერირება. ამ თემასთან დაკავშირებით აზრი ორად იყოფა - ზოგს მიაჩნია რომ უმჯობესია useRef ჰუკის გამოყენება, ზოგი კი useState ჰუკს ანიჭებს უპირატესობას. თუმცა, დიდი ზომის ფორმებთან სამუშაოდ useState ჰუკის გამოყენება ჯობია :

  • useState-ის გამოყენება გულისხმობს, რომ სამომხმარებლო ინტერფეისი ავტომატურად განახლდება როდესაც ფორმაში რაიმეს შევცვლით.
  • useRef მოსახერხებელია პატარა ზომის ფორმებთან მუშაობისას, ან მაშინ, როდესაც საჭირო არ არის ფორმის ცვლილებების სამომხმარებლო ინტერფეისში დაუყოვნებლივ ასახვა.

React Hook Form ბიბლიოთეკა

React Hook Form არის ბიბლიოთეკა, რომელიც გვიმარტივებს ფორმებთან მუშაობას. მის დასაინსტალირებლად გავუშვათ შემდეგი ბრძანება :

npm i react-hook-form                                                         
                

გავმართოთ ჩვენი ფორმა React Hook Form ბიბლიოთეკის მეშვეობით :

import { useForm } from "react-hook-form";

const Form = () => {
    const form = useForm()
    console.log(form)  // ობიექტი, რომელშიც აღწერილია ფორმებთან სამუშაო მეთოდები

    const { register, handleSubmit } = useForm();

    return (
        <form onSubmit={handleSubmit((data) => console.log(data))}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input
                    type="text"
                    className="form-control"
                    id="name"
                    {...register("name")}
                />
            </div>
            <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input
                    type="number"
                    className="form-control"
                    id="age"
                    {...register("age")}
                />
            </div>
            <button className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;                                                                         
                

register მეთოდის გამოყენებისას React Hook Form ბიბლიოთეკას ვეუბნებით : 'ეს ველი ფორმის ნაწილია', 'მიიღე ამ ველში აკრეფილი ინფორმაცია და შეიტანე ის გასაგზავნ ინფორმაციათა ნუსხაში, როდესაც ფორმა გაიგზავნება'.

handleSubmit მეთოდი კი გამოიყენება ფორმის გაგზავნისათვის : ახდენს register მეთოდით დარეგისტრირებულ ველებში აკრეფილი ინფორმაციების ვალიდაციას, აერთიანებს მათ ერთ ობიექტში და ამ ობიექტს აწვდის არგუმენტად გადაცემულ უკუფუნქციას (ჩვენს შემთხვევაში ისარფუნქცია: (data) => console.log(data)). ღილაკზე დაჭერის შემდეგ შესრულდება ის ინსტრუქციები რაც უკუფუნქციაში იქნება აღწერილი (console.log(data)).

ბუნებრივია რეალურ პროექტებში შედარებით უფრო რთული ამოცანები სრულდება ხოლმე ფორმის გაგზავნისას, ვიდრე console.log()-ია, ამიტომ უკუფუნქციების აღწერა handleSubmit მეთოდის გარეთ უმჯობესია :

import { FieldValues, useForm } from "react-hook-form";

const Form = () => {
    const { register, handleSubmit } = useForm();
    const customOnSubmit = (data:FieldValues) => console.log(data);

    return (
        <form onSubmit={handleSubmit(customOnSubmit)}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input
                    type="text"
                    className="form-control"
                    id="name"
                    {...register("name")}
                />
            </div>
            <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input
                    type="number"
                    className="form-control"
                    id="age"
                    {...register("age")}
                />
            </div>
            <button className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;                                                                                  
                

FieldValues არის React Hook Form ბიბლიოთეკაში ჩადგმული ტიპი, რომელიც განსაზღვრავს ფორმაში აკრეფილი ინფორმაციებით შექმნილი ობიექტის ძირითად სტრუქტურას. ამ ტიპის მითითებისას ტაიპსკრიპტი ხვდება, რომ ინფორმაციას ექნება 'გასაღები-მნიშვნელობა' წყვილებით შედგენილი ობიექტის სახე :

{
    name: "ვასო",
    age: 36
}                                                                    
                

ალბათ შეამჩნევდით, ფორმა გაცილებით უფრო მარტივად აღვწერეთ React Hook Form ბიბლიოთეკის მეშვეობით, ვიდრე ეს useState ჰუკით გავაკეთეთ.

ფორმის ვალიდაცია

დავამატოთ ჩვენს ფორმას ვალიდაციები :

import { FieldValues, useForm } from "react-hook-form";

const Form = () => {
    const {
        register,
        handleSubmit,
        formState: { errors },
    } = useForm();

    const customOnSubmit = (data: FieldValues) => console.log(data);

    return (
        <form onSubmit={handleSubmit(customOnSubmit)}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input
                    type="text"
                    className="form-control"
                    id="name"
                    {...register("name", { required: true, minLength: 2 })}
                />
                {errors.name?.type === "required" && (
                    <p className="text-danger">სახელის მითითება აუცილებელია</p>
                )}
                {errors.name?.type === "minLength" && (
                    <p className="text-danger">
                    სახელი უნდა შეიცავდეს ერთ სიმბოლოზე მეტს
                    </p>
                )}
            </div>
            <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input
                    type="number"
                    className="form-control"
                    id="age"
                    {...register("age", { required: true })}
                />
                {errors.age?.type === "required" && (
                    <p className="text-danger">ასაკის მითითება აუცილებელია</p>
                )}
            </div>
            <button className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;                                                                                             
                

React Hook Form ბიბლიოთეკაში formState არის ობიექტი, რომელშიც ასახულია ფორმის მიმდინარე მდგომარეობა. იგი მოიცავს ინფორმაციას იმის შესახებ თუ რომელი ველები შეივსო, რომელი ველების შევსებისას დაფიქსირდა ვალიდაციის შეცდომები და ა.შ.

console.log(formState);
                


formState: { errors } ჩანაწერით, ჩვენ უბრალოდ მივწვდით formState ობიექტის იმ თვისებას, რომელშიც შენახეულია ინფორმაცია ვალიდაციის შეცდომების შესახებ.

იმისათვის, რათა უფრო გაგვიადვილდეს ველებთან წვდომა და ტაიპსკრიპტის კომპილატორმაც ზუსტად იცოდეს თუ რა ველებთან ვმუშაობთ, აღვწეროთ ინტერფეისი და გადავცეთ იგი useForm-ს :

import { FieldValues, useForm } from "react-hook-form";

interface FormData {
    name: string;
    age: number;
}

const Form = () => {
    const {
        register,
        handleSubmit,
        formState: { errors },
    } = useForm<FormData>();

    const customOnSubmit = (data: FieldValues) => console.log(data);

    return (
        <form onSubmit={handleSubmit(customOnSubmit)}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input
                    type="text"
                    className="form-control"
                    id="name"
                    {...register("name", { required: true, minLength: 2 })}
                />
                {errors.name?.type === "required" && (
                    <p className="text-danger">სახელის მითითება აუცილებელია</p>
                )}
                {errors.name?.type === "minLength" && (
                    <p className="text-danger">
                    სახელი უნდა შეიცავდეს ერთ სიმბოლოზე მეტს
                    </p>
                )}
            </div>
            <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input
                    type="number"
                    className="form-control"
                    id="age"
                    {...register("age", { required: true })}
                />
                {errors.age?.type === "required" && (
                    <p className="text-danger">ასაკის მითითება აუცილებელია</p>
                )}
            </div>
            <button className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;                                                                                                         
                

ამის შემდეგ კომპილატორსაც ექნება წარმოდგენა ჩვენი ფორმის ველებზე და ედიტორიც დაგვეხმარება ველთა დასახელებების ავტოდამთავრებით :



სქემაზე დაფუძნებული ვალიდაცია

სქემაზე დაფუძნებული ვალიდაცია არის ვალიდაციის მეთოდი, რომელიც იყენებს ვალიდაციის წესების წინასწარგანსაზღვრულ სტრუქტურას, სქემას.

არსებობს სქემაზე დაფუძნებულ ვალიდაციებთან სამუშაო სხვადასხვა ბიბლიოთეკები: Yup, Joi, Zod...

Zod ბიბლიოთეკა

Zod ბიბლიოთეკის დასაინსტალირებლად გავუშვათ შემდეგი ბრძანება :

npm i zod@3.20.6                                                                                     
                

ახალ ბიბლიოთეკაზე გადაწყობის შემდეგ ჩვენს ფორმას ექნება ასეთი სახე :

import { FieldValues, useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod/src/zod.ts";

// ვალიდაციის სტრუქტურა, სქემა
const schema = z.object({
    name: z.string().min(2),
    age: z.number().min(18),
});

type FormData = z.infer<typeof schema>;

const Form = () => {
    const {
        register,
        handleSubmit,
        formState: { errors },
    } = useForm<FormData>({ resolver: zodResolver(schema) });

    const customOnSubmit = (data: FieldValues) => console.log(data);

    return (
        <form onSubmit={handleSubmit(customOnSubmit)}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input
                    type="text"
                    className="form-control"
                    id="name"
                    {...register("name")}
                />
                {errors.name && <p className="text-danger">{errors.name.message}</p>}
            </div>
            <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input
                    type="number"
                    className="form-control"
                    id="age"
                    {...register("age", { valueAsNumber: true })}
                />
                {errors.age && <p className="text-danger">{errors.age.message}</p>}
            </div>
            <button className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;                                                                                                       
                

განვიხილოთ კოდის საკვანძო წერტილები.

zodResolver არის ერთგვარი ხიდი React Hook Form და Zod ბიბლიოთეკებს შორის. იგი Zod-ის ვალიდაციის შედეგებს გარდაქმნის React Hook Form -ისათვის გასაგებ ფორმატში. დასაინსტალირებლად უნდა გავუშვათ შემდეგი ბრძანება :

npm i @hookform/resolvers@2.9.11
                

ამ ჩანაწერით :

useForm({ resolver: zodResolver(schema) })
                

მოვახდინეთ Zod ბიბლიოთეკის ინტეგრაცია React Hook Form ბიბლიოთეკაში.

  1. როდესაც მომხმარებელი დააწვება გაგზავნის ღილაკს, მოხდება handleSubmit მეთოდის გამოძახება.
  2. zodResolver-ი Zod-ის მეშვეობით დაადგენს ვალიდურია თუ არა აკრეფილი ინფორმაცია აღწერილ სქემასთან მიმართებაში.
  3. თუ:
    • ვალიდაცია წარმატებულია: აკრეფილი ინფორმაცია გადაეცემა onSubmit ფუნქციას.
    • ვალიდაცია წარუმატებელია: შეიქმნება errors ობიექტი რომელშიც შეინახება Zod ვალიდაციის შეტყობინებები.

Zod ბიბლიოთეკაში არსებული infer მეთოდი საშუალებას გვაძლევს აღწერილი სქემის მიხედვით შევქმნათ, ჩვენი ფორმის შესაბამისი, ტაიპსკიპტის ახალი ტიპი (რაც ადრე ინტერფეისის მეშვეობით გავაკეთეთ) :

type FormData = z.infer<typeof schema>;                                                                                    
                

თუ მაუსს FormData ტიპთან მივიტანთ, ვიხილავთ ასეთ სურათს :



ვფიქრობ კოდის დანარჩენ ნაწილში ყველაფერი გასაგებია. ასევე უნდა აღინიშნოს, რომ ჩვენ განვიხილეთ Zod ბიბლიოთეკის გამოყენების უმარტივესი მაგალითი. მას გაცილებით დიდი ფუნქციონალი და მეტი შესაძლებლობები გააჩნია, რომელთა გაცნობაც ოფიციალურ ვებგვერდზეა შესაძლებელი.

ღილაკის გათიშვა

იმ შემთხვევაში, თუ ვალიდაციის პროცესი წარუმატებელად დასრულდება, გავთიშოთ ღილაკი :

import { FieldValues, useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod/src/zod.ts";

const schema = z.object({
    name: z.string().min(2),
    age: z.number().min(18),
});

type FormData = z.infer<typeof schema>;

const Form = () => {
    const {
        register,
        handleSubmit,
        formState: { errors, isValid },
    } = useForm<FormData>({ resolver: zodResolver(schema) });

    const customOnSubmit = (data: FieldValues) => console.log(data);

    return (
        <form onSubmit={handleSubmit(customOnSubmit)}>
            <div className="mb-3">
                <label htmlFor="name" className="form-label">
                    სახელი
                </label>
                <input
                    type="text"
                    className="form-control"
                    id="name"
                    {...register("name")}
                />
                {errors.name && <p className="text-danger">{errors.name.message}</p>}
            </div>
            <div className="mb-3">
                <label htmlFor="age" className="form-label">
                    ასაკი
                </label>
                <input
                    type="number"
                    className="form-control"
                    id="age"
                    {...register("age", { valueAsNumber: true })}
                />
                {errors.age && <p className="text-danger">{errors.age.message}</p>}
            </div>
            <button disabled={!isValid} className="btn btn-primary" type="submit">
                გაგზავნა
            </button>
        </form>
    );
};

export default Form;                                       
                
12. მინიატურული პროექტი - ხარჯთაღრიცხვის ცხრილი

კურსის ამ თავში გავაერთიანოთ ყველაფერი, რაც აქამდე ვისწავლეთ და შევქმნათ ხარჯთაღრიცხვის საწარმოებელი პროექტი. პროექტში შეგვეძლება ხარჯთა დამატება კატეგორიების მიხედვით, მათი გაფილტრვა, წაშლა და ჯამური ხარჯის დაანგარიშება.

მაშ ასე, შევუდგეთ საქმეს 😎

შევქმნათ საქაღალდე /src/expense-tracker, მასში - საქაღალდე components, ამ უკანასკნელში კი ფაილი ExpenseList.tsx შემდეგი კოდით :

interface Expense {
    id: number;
    description: string;
    amount: number;
    category: string;
}

interface Props {
    expenses: Expense[];
    onDelete: (id: number) => void;
}

const ExpenseList = ({ expenses, onDelete }: Props) => {
    return (
        <table className="table table-bordered">
        <thead>
            <tr>
                <th>აღწერა</th>
                <th>თანხა</th>
                <th>კატეგორია</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            {expenses.map((expense) => (
            <tr key={expense.id}>
                <td>{expense.description}</td>
                <td>{expense.amount}</td>
                <td>{expense.category}</td>
                <td>
                <button
                    className="btn btn-outline-danger"
                    onClick={() => onDelete(expense.id)}
                >
                    წაშლა
                </button>
                </td>
            </tr>
            ))}
        </tbody>
        {expenses.length !== 0 && (
            <tfoot>
            <tr>
                <td>ჯამი</td>
                <td>
                ₾
                {expenses
                    .reduce((acc, expense) => expense.amount + acc, 0)
                    .toFixed(2)}
                </td>
                <td></td>
                <td></td>
            </tr>
            </tfoot>
        )}
        </table>
    );
};

export default ExpenseList;                                                  
                

ამ კოდში ისეთი არაფერია, რაზეც აქამდე არ გვისაუბრია და, წესით და რიგით, ყველაფერი გასაგები უნდა იყოს 🥰 ერთადერთი - განვიხილოთ ჯამური თანხის დაანგარიშების ფრაგმენტი :

expenses.reduce((acc, expense) => expense.amount + acc, 0).toFixed(2)
                
  1. რას აკეთებს reduce მეთოდი:
    reduce მეთოდი იღებს მასივს (ამ შემთხვევაში expenses) და დაჰყავს იგი ერთ კონკრეტულ მნიშვნელობამდე მასივის თითოეულ ელემენტზე უკუფუნქციის მიმაგრების გზით.

  2. reduce მეთოდის პარამეტრები:

    • პირველი პარამეტრი (acc) არის აკუმულატორი (accumulator), 'შემნახველი საკანი', რომელშიც ინახება მიმდინარე ან საბოლოო მნიშვნელობა.
    • მეორე პარამეტრი (expense) არის მიმდინარე ელემენტი, რომლის დამუშავებაც ხდება თითოეულ იტერაციაზე.
  3. საინიციალიზაციო მნიშვნელობა (0):

    • აკუმულატორის საწყისი მნიშვნელობა არის acc = 0.
  4. უკუფუნქციის ლოგიკა:
    მასივში არსებული თითოეული expense ობიექტისათვის:

    • დაემატოს მიმდინარე expense.amount აკუმულატორს.
  5. საფინალო შედეგი:
    მასივის ყველა ელემენტის გავლის შემდეგ reduce მეთოდი აბრუნებს აკუმულატორის საბოლოო შედეგს, ანუ თანხების მთლიან ჯამს.

დავუბრუნდეთ ჩვენს კომპონენტებს. /src/App.tsx ფაილში შევიტანოთ შემდეგი კოდი :

import { useState } from "react";
import ExpenseList from "./expense-tracker/components/ExpenseList";

function App() {
    const [expenses, setExpenses] = useState([
        { id: 1, description: "კლავიატურა", amount: 10, category: "ტექნიკა" },
        { id: 2, description: "პროცესორი", amount: 100, category: "ტექნიკა" },
        { id: 3, description: "პური", amount: 1, category: "საკვები" },
        { id: 4, description: "ლიმონათი", amount: 10, category: "სასმელი" },
    ]);

    return (
        <div>
            <ExpenseList
                expenses={expenses}
                onDelete={(id) => {
                    setExpenses(expenses.filter((e) => e.id !== id));
                }}
            />
        </div>
    );
}

export default App;
                

ხარჯების ფილტრი

/src/expense-tracker/components საქაღალდეში შევქმნათ ფაილი ExpenseFilter.tsx შემდეგი კოდით :

interface Props {
    onSelectCategory: (category: string) => void;
}
    
const ExpenseFilter = ({ onSelectCategory }: Props) => {
    return (
        <select
            className="form-select"
            onChange={(event) => onSelectCategory(event.target.value)}
        >
            <option value="">ყველა კატეგორია</option>
            <option value="ტექნიკა">ტექნიკა</option>
            <option value="საკვები">საკვები</option>
            <option value="სასმელი">სასმელი</option>
        </select>
    );
};
    
export default ExpenseFilter;                      
                

გამოვიყენოთ ახლადშექმნილი ფილტრი /src/App.tsx ფაილში :

import { useState } from "react";
import ExpenseList from "./expense-tracker/components/ExpenseList";
import ExpenseFilter from "./expense-tracker/components/ExpenseFilter";

function App() {
    const [selectedCategory, setSelectedCategory] = useState("");

    const [expenses, setExpenses] = useState([
        { id: 1, description: "კლავიატურა", amount: 10, category: "ტექნიკა" },
        { id: 2, description: "პროცესორი", amount: 100, category: "ტექნიკა" },
        { id: 3, description: "პური", amount: 1, category: "საკვები" },
        { id: 4, description: "ლიმონათი", amount: 10, category: "სასმელი" },
    ]);

    const visibleExpenses = selectedCategory
        ? expenses.filter((e) => e.category == selectedCategory)
        : expenses;

    return (
        <div>
            <div className="mb-3 mt-2">
                <ExpenseFilter
                    onSelectCategory={(category) => {
                    setSelectedCategory(category);
                    }}
                />
            </div>
            <ExpenseList
                expenses={visibleExpenses}
                onDelete={(id) => {
                    setExpenses(expenses.filter((e) => e.id !== id));
                }}
            />
        </div>
    );
}

export default App;                    
                

ხარჯის დამატება სიაში

დამატების ფორმა

/src/expense-tracker/components საქაღალდეში გავაკეთოთ ფაილი ExpenseForm.tsx შემდეგი კოდით :

import { categories } from "../../App";

const ExpenseForm = () => {
    return (
        <form>
            <div className="mb-3">
                <label htmlFor="description" className="form-label">
                    აღწერა
                </label>
                <input type="text" className="form-control" id="description" />
            </div>
            <div className="mb-3">
                <label htmlFor="amount" className="form-label">
                    თანხა
                </label>
                <input type="number" className="form-control" id="amount" />
            </div>
            <div className="mb-3">
                <label htmlFor="category" className="form-label">
                    კატეგორია
                </label>
                <select id="category" className="form-select">
                    <option value=""></option>
                    {categories.map((category) => (
                    <option value={category} key={category}>
                        {category}
                    </option>
                    ))}
                </select>
            </div>
            <button className="btn btn-primary">დამატება</button>
        </form>
    );
};

export default ExpenseForm;                                 
                

გამოვიძახოთ ფორმა /src/App.tsx ფაილში :

import { useState } from "react";
import ExpenseList from "./expense-tracker/components/ExpenseList";
import ExpenseFilter from "./expense-tracker/components/ExpenseFilter";
import ExpenseForm from "./expense-tracker/components/ExpenseForm";

export const categories = ["ტექნიკა", "საკვები", "სასმელი"] as const;

function App() {
    const [selectedCategory, setSelectedCategory] = useState("");

    const [expenses, setExpenses] = useState([
        { id: 1, description: "კლავიატურა", amount: 10, category: "ტექნიკა" },
        { id: 2, description: "პროცესორი", amount: 100, category: "ტექნიკა" },
        { id: 3, description: "პური", amount: 1, category: "საკვები" },
        { id: 4, description: "ლიმონათი", amount: 10, category: "სასმელი" },
    ]);

    const visibleExpenses = selectedCategory
        ? expenses.filter((e) => e.category == selectedCategory)
        : expenses;

    return (
        <div>
                <div className="mb-5 mt-2">
                    <ExpenseForm />
                </div>
                <div className="mb-3 mt-2">
                    <ExpenseFilter
                            onSelectCategory={(category) => {
                                setSelectedCategory(category);
                            }}
                    />
                </div>
                <ExpenseList
                    expenses={visibleExpenses}
                    onDelete={(id) => {
                        setExpenses(expenses.filter((e) => e.id !== id));
                    }}
                />
        </div>
    );
}

export default App;                       
                

როგორც ხედავთ, კატეგორიების ჩამონათვალი App კომპონენტში აღვწერეთ და ეს ჩამონათვალი გამოვიყენოთ ხოლმე select ელემენტებში.

ExpenseFilter.tsx ფაილი :

import { categories } from "../../App";

interface Props {
    onSelectCategory: (category: string) => void;
}
  
const ExpenseFilter = ({ onSelectCategory }: Props) => {
    return (
        <select
            className="form-select"
            onChange={(event) => onSelectCategory(event.target.value)}
        >
            <option value="">ყველა კატეგორია</option>
            {categories.map((category) => (
                <option value={category} key={category}>{category}</option>
            ))}
        </select>
    );
};

export default ExpenseFilter;      
                
ფორმის ვალიდაცია

ExpenseForm.tsx ფაილი :

import { z } from "zod";
import { categories } from "../../App";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod/src/zod.ts";

const schema = z.object({
    description: z.string().min(3).max(50),
    amount: z.number().min(0.01).max(10000),
    category: z.enum(categories), // უნდა იყოს categories მნიშვნელობებიდან ერთ-ერთი 
});

type ExpenseFormData = z.infer<typeof schema>;

const ExpenseForm = () => {
    const {
        register,
        handleSubmit,
        formState: { errors },
    } = useForm<ExpenseFormData>({ resolver: zodResolver(schema) });

    return (
        <form onSubmit={handleSubmit((data) => console.log(data))}>
            <div className="mb-3">
                <label htmlFor="description" className="form-label">
                    აღწერა
                </label>
                <input
                    {...register("description")}
                    type="text"
                    className="form-control"
                    id="description"
                />
                {errors.description && (
                    <p className="text-danger">{errors.description.message}</p>
                )}
            </div>
            <div className="mb-3">
                <label htmlFor="amount" className="form-label">
                    თანხა
                </label>
                <input
                    {...register("amount", { valueAsNumber: true })}
                    type="number"
                    className="form-control"
                    id="amount"
                />
                {errors.amount && (
                    <p className="text-danger">{errors.amount.message}</p>
                )}
            </div>
            <div className="mb-3">
                <label htmlFor="category" className="form-label">
                    კატეგორია
                </label>
                <select {...register("category")} id="category" className="form-select">
                    <option value=""></option>
                    {categories.map((category) => (
                    <option value={category} key={category}>
                        {category}
                    </option>
                    ))}
                </select>
                {errors.category && (
                    <p className="text-danger">{errors.category.message}</p>
                )}
            </div>
            <button className="btn btn-primary">დამატება</button>
        </form>
    );
};

export default ExpenseForm;  
                

თუ კონსოლს გადავამოწმებთ, ვიხილავთ ამდაგვარ შეტყობინებას :

ExpenseForm.tsx:9 Uncaught ReferenceError: Cannot access 'categories' before initialization 
                

შეცდომა ფიქსირდება ExpenseForm.tsx ფაილში :


საქმე იმაშია, რომ categories კონსტანტას ვაიმპორტებთ App კომპონენტიდან :

import { categories } from "../../App";                                                        
                

App კომპონენტში კი გვაქვს ასეთი სიტუაცია :

import { useState } from "react";
import ExpenseList from "./expense-tracker/components/ExpenseList";
import ExpenseForm from "./expense-tracker/components/ExpenseForm";
import ExpenseFilter from "./expense-tracker/components/ExpenseFilter";

export const categories = ["ტექნიკა", "საკვები", "სასმელი"] as const;

...
                

ანუ ჯერ ვიძახებთ ExpenseForm კომპონენტს და მხოლოდ ამის შემდეგ ხდება კონსტანტის აღწერა. შესაბამისად - კონსტანტა არ არის ხილვადი ამ კომპონენტში.

კატეგორიების კონსტანტა განვათავსოთ ისეთ ადგილას, სადაც უპრობლემოდ მივწვდებით მას აპლიკაციის ყველა წერტილიდან და გამოძახებების თანმიმდევრობას მნიშვნელობა არ ექნება. expense-tracker საქაღალდეში შევქმნათ ფაილი categories.ts :

const categories = ["ტექნიკა", "საკვები", "სასმელი"] as const;

export default categories;                    
                

შევცვალოთ categories კონსტანტის იმპორტებიც ExpenseFilter.tsx და ExpenseForm.tsx ფაილებში :

import categories from "../categories";                    
                

App.tsx ფაილში კი წავშალოთ ეს ჩანაწერი :

export const categories = ["ტექნიკა", "საკვები", "სასმელი"] as const;  
                
ფორმის გაგზავნა

ExpenseForm.tsx ფაილი :

import { z } from "zod";
import categories from "../categories";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod/src/zod.ts";

const schema = z.object({
    description: z.string().min(3).max(50),
    amount: z.number().min(0.01).max(10000),
    category: z.enum(categories), // უნდა იყოს categories მნიშვნელობებიდან ერთ-ერთი 
});

type ExpenseFormData = z.infer<typeof schema>;

interface Props {
    onSubmit: (data: ExpenseFormData) => void;
}

const ExpenseForm = ({ onSubmit }: Props) => {
    const {
        register,
        handleSubmit,
        formState: { errors },
    } = useForm<ExpenseFormData>({ resolver: zodResolver(schema) });

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <div className="mb-3">
                <label htmlFor="description" className="form-label">
                    აღწერა
                </label>
                <input
                    {...register("description")}
                    type="text"
                    className="form-control"
                    id="description"
                />
                {errors.description && (
                    <p className="text-danger">{errors.description.message}</p>
                )}
            </div>
            <div className="mb-3">
                <label htmlFor="amount" className="form-label">
                    თანხა
                </label>
                <input
                    {...register("amount", { valueAsNumber: true })}
                    type="number"
                    className="form-control"
                    id="amount"
                />
                {errors.amount && (
                    <p className="text-danger">{errors.amount.message}</p>
                )}
            </div>
            <div className="mb-3">
                <label htmlFor="category" className="form-label">
                    კატეგორია
                </label>
                <select {...register("category")} id="category" className="form-select">
                    <option value=""></option>
                    {categories.map((category) => (
                    <option value={category} key={category}>
                        {category}
                    </option>
                    ))}
                </select>
                {errors.category && (
                    <p className="text-danger">{errors.category.message}</p>
                )}
            </div>
            <button className="btn btn-primary">დამატება</button>
        </form>
    );
};

export default ExpenseForm;  
                

ჩავასწოროთ ExpenseForm კომპონენტის გამოძახების ფრაგმენტი App.tsx ფაილში :

<ExpenseForm
        onSubmit={(expense) =>
        setExpenses([...expenses, { ...expense, id: expenses.length + 1 }])
    }
/>              
                

onSubmit ატრიბუტს არგუმენტად გადაეცა ისარფუნქცია, რომელმაც expense ცვლადში გააერთიანა ფორმაში აკრეფილი ინფორმაცია ანუ ახალი ხარჯი, შემდეგ გამოიძახა setExpenses მეთოდი და მას არგუმენტად გადასცა უკვე არსებული ხარჯები და პლიუს ახალი ხარჯი.

ალბათ შეამჩნევდით, რომ ხარჯის დამატების შემდეგ, ფორმა ისევ შევსებული რჩება. გამოვასწოროთ ეს. ExpenseForm.tsx ფაილი :

import { z } from "zod";
import categories from "../categories";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod/src/zod.ts";

const schema = z.object({
    description: z.string().min(3).max(50),
    amount: z.number().min(0.01).max(10000),
    category: z.enum(categories), // უნდა იყოს categories მნიშვნელობებიდან ერთ-ერთი
});

type ExpenseFormData = z.infer<typeof schema>;

interface Props {
    onSubmit: (data: ExpenseFormData) => void;
}

const ExpenseForm = ({ onSubmit }: Props) => {
    const {
        register,
        handleSubmit,
        reset,
        formState: { errors },
    } = useForm<ExpenseFormData>({ resolver: zodResolver(schema) });

    return (
        <form
            onSubmit={handleSubmit((data) => {
                onSubmit(data); 
                reset();
            })}
        >
            <div className="mb-3">
                <label htmlFor="description" className="form-label">
                    აღწერა
                </label>
                <input
                    {...register("description")}
                    type="text"
                    className="form-control"
                    id="description"
                />
                {errors.description && (
                    <p className="text-danger">{errors.description.message}</p>
                )}
            </div>
            <div className="mb-3">
                <label htmlFor="amount" className="form-label">
                    თანხა
                </label>
                <input
                    {...register("amount", { valueAsNumber: true })}
                    type="number"
                    className="form-control"
                    id="amount"
                />
                {errors.amount && (
                    <p className="text-danger">{errors.amount.message}</p>
                )}
            </div>
            <div className="mb-3">
                <label htmlFor="category" className="form-label">
                    კატეგორია
                </label>
                <select {...register("category")} id="category" className="form-select">
                    <option value=""></option>
                    {categories.map((category) => (
                    <option value={category} key={category}>
                        {category}
                    </option>
                    ))}
                </select>
                {errors.category && (
                    <p className="text-danger">{errors.category.message}</p>
                )}
            </div>
            <button className="btn btn-primary">დამატება</button>
        </form>
    );
};

export default ExpenseForm;                                 
                

მაშ ასე, ჩვენი მინიატურული პროექტი დასრულდა ✅ წინ კი ძალიან საინტერესო საკითხები გველოდება 🤩

13. useEffect ჰუკი

useEffect ჰუკი

useEffect არის რეაქტში არსებული კიდევ ერთი ჰუკი, რომელიც საშუალებას გვაძლევს შევასრულოთ კონკრეტული ინსტრუქციები კომპონენტის სასიცოცხლო ციკლის კონკრეტულ მომენტებში. useEffect ჰუკი, როგორც წესი, გვერდითი პროცესების (side effects) სამართავად გამოიყენება.

გვერდითი პროცესი შეიძლება ეწოდოს ნებისმიერ ოპერაციას, რომელიც, სამომხარებლო ინტერფეისის გენერირების მიღმა, მის გარეთ, მის პარალელურად მიმდინარეობს. ერთ-ერთი ასეთი ოპერაციაა სერვერიდან ინფორმაციის წამოღება :

  • ეს ხდება კომპონენტის გარეთ (ვაკითხავთ სერვერს).
  • სამომხარებლო ინტერფეისი არ არის მყისიერად დამოკიდებული სერვერზე - განახლდება მაშინ, როდესაც ინფორმაცია მოვა სერვერიდან.
  • ეს არის ასინქრონული ოპერაცია - ესაჭიროება დრო, თუმცა არ ბლოკავს სამომხარებლო ინტერფეისს.

useEffect ჰუკის გამოყენების სინტაქსი ასეთია :

useEffect(<ფუნქცია>, <დამოკიდებულება>)                    
                

კონკრეტული მაგალითი კი ასეთი :

import { useEffect, useState } from "react";

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

    useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts")
        .then((response) => response.json())
        .then((json) => setData(json));
    }, []); 

    return (
        <iv>
            <h1>სიახლეები
            <ul>
                {data.map((post) => (
                    <li key={post.id}>{post.title}</li>
                ))}
            </ul>
        </div>
    );
}                               
                

ამ მაგალითში :

  1. რეაქტი დააგენერირებს კომპონენტს.
  2. useEffect შესრულდება მხოლოდ ერთხელ, პირველი გენერირების შემდეგ - გაიგზავნება მოთხოვნა სერვერზე.
  3. როდესაც პასუხი მოვა, setData(json) განაახლებს მდგომარეობას.
  4. რეაქტი განაახლებს კომპონენტს და გამოიტანს მიღებულ ინფორმაციას.

useEffect ჰუკის მართვა

მოყვანილ მაგალითში useEffect ჰუკი გაეშვება მხოლოდ ერთხელ - მას შემდეგ რაც კომპონენტი პირველად ჩაიტვირთება.

რა არის დამოკიდებულება useEffect ჰუკში ?

დამოკიდებულება არის მნიშვნელობა (state ან prop), რომლის ცვლილებასაც useEffect ჰუკის ხელახალი გაშვება უნდა მოჰყვეს.

დამოკიდებულებების აღწერა ხდება ჰუკის მეორე, არასავალდებულო პარამეტრში, რომელიც წარმოადგენს დამოკიდებულებების მასივს. დამოკიდებულებად შეიძლება ჩაჯდეს კომპონენტის კონკრეტული მდგომარეობა (state), ან კონკრეტული რეკვიზიტი (prop). როდესაც დამოკიდებულების მნიშვნელობა იცვლება, useEffect ჰუკის პირველ პარამეტრში აღწერილი ინსტრუქციებიც ხელახლა სრულდება.

არსებობს useEffect ჰუკის გამოყენების სხვადასხვა ვარიანტები :

1. დამოკიდებულებათა გარეშე (გაეშვება ყოველ გენერირებაზე)

დავუშვათ გვსურს კონსოლში გამოვიტანოთ რაიმე შეტყობინება კომპონენტის ყოველი გენერირებისას :

useEffect(() => {
    console.log("კომპონენტი დაგენერირდა"); // გაეშვება კომპონენტის ყოველ გენერირებაზე
});                 
                
2. დამოკიდებულებათა ცარიელი მასივი [] (გაეშვება მხოლოდ ერთხელ - კომპონენტის პირველი გენერირებისას)

დავუშვათ გვსურს სერვერიდან წამოვიღოთ ინფორმაცია მხოლოდ ერთხელ, კომპონენტის პირველი გენერირებისას :

useEffect(() => {
    fetch(...) // გაეშვება მხოლოდ ერთხელ - კომპონენტის პირველი გენერირებისას 
}, []);               
                
3. დამოკიდებულებათა არაცარიელი მასივი [prop, state] (გაეშვება მხოლოდ კონკრეტული (!) დამოკიდებულებების ცვლილებაზე)

წარმოვიდგინოთ ასეთი სიტუაცია : მონაცემთა ბაზაში შენახული გვაქვს პროდუქტი კატეგორიების მიხედვით. კომპონენტის საწყისი ჩატვირთვისას უნდა წამოვიღოთ ყველა პროდუქტი, ხოლო თუ მომხმარებელი რომელიმე კატეგორიას აირჩევს, მაშინ ხელახლა უნდა მივმართოთ სერვერს და პროდუქტი არჩეული კატეგორიის მიხედვით გავფილტროთ, ეს უნდა გავაკეთოთ კატეგორიის ყოველ ცვლილებაზე :

const [category, setCategory] = useState('');

useEffect(() => {
    // გაეშვება პირველ გენერირებაზე
    // და ყოველ ჯერზე როდესაც შეიცვლება მხოლოდ (!) კატეგორიის მნიშვნელობა (სხვა მიზეზით არასოდეს)
}, [category]);              
                

ანუ ჰუკის გაშვება / არგაშვება დამოკიდებულია კატეგორიის ცვლილებაზე.

სიტუაცია ეშვება პირველი გენერირებისას? ეშვება ყოველი გენერირებისას? ეშვება მხოლოდ კონკრეტული დამოკიდებულების ცვლილებისას?
useEffect(() => { ... }); ✅ დიახ ✅ დიახ ✅ ყოველთვის
useEffect(() => { ... }, []); ✅ დიახ ❌ არა ❌ არა
useEffect(() => { ... }, [prop, state]); ✅ დიახ ❌ არა ✅ დიახ

ეს არის useEffect ჰუკის გამოყენების შესაძლო ვარიანტები.

გვერდითი პროცესების გაუქმება

როგორც აღვნიშნეთ, useEffect გამოიყენება ე.წ გვერდით პროცესების სამართავად. ხანდახან საჭიროა useEffect ჰუკის გამოძახების შედეგად გაშვებული პროცესების გაუქმება, გამოძახების შედეგების ანულირება, გასუფთავება, 'გამსუფთავებელი'კი არის ჩვეულებრივი ფუნქცია, რომელშიც საშუალებას გვაძლევს, საჭიროებას შემთხვევაში, გავაუქმოთ მიმდინარე გვერდითი პროცესი. თავად ფუნქცია კი useEffect ჰუკის ხელახალი გაშვების, ან კომპონენტის დემონტაჟის წინ სრულდება.

დავუშვათ ინფორმაციას ვიღებთ სერვერიდან და პასუხის დაბრუნებამდე სხვა გვერდზე გადადის მომხმარებელი. რა ხდება ამ დროს ?

import { useState, useEffect } from "react";

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

    useEffect(() => {
    fetch("https://api.example.com/data")
        .then((res) => res.json())
        ❌ დავუშვათ ამ დროს გადავიდა მომხმარებელი სხვა გვერდზე
        .then((result) => {
            setData(result); // ❌ დემონტრაჟის შემდეგ მდგომარეობის განახლების საშიშროება
        });

    }, []);

    return <pre>{JSON.stringify(data, null, 2)}</pre>;
}                             
                
  1. კომპონენტი ჩაიტვირთება → გაეშვება useEffect → სერვერზე გაიგზავნება მოთხოვნა.
  2. მომხმარებელი მოთხოვნის დასრულებამდე გადადის სხვა გვერდზედაგენერირდება სხვა კომპონენტი, მოხდება ძველის დემონტაჟი (unmount).
  3. API მოთხოვნა მაინც შესრულდება (ჯავასკრიპტი ავტომატურად არ აუქმებს მას).
  4. .then() ბლოკი შეეცდება განახლოს მდგომარეობა არარსებულ კომპონენტში.
  5. რეაქტი დააგენერირებს გაფრთხილებას:
    "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak."
                            
    ანუ : შეუძლებელია მდგომარეობის განახლება არარსებულ, დემონტაჟირებულ კომპონენტში. "No-op", ანუ იგივე "No Operation" ნიშნავს, რომ რეაქტი დააიგნორებს არარსებულ კომპონენტში მდგომარეობის განახლების პროცესს, თუმცა ინფრომაციის წამოღების მოთხოვნა მაინც აქტიური იქნება, მეხსიერებაში კი არასაჭირო ინფორმაცია დაგროვდება.

ასეთ დროს, საუკეთესო ვარიანტია AbortController-ის გამოყენება.

useEffect(() => {
    const controller = new AbortController(); 
    const signal = controller.signal; 

    fetch("https://api.example.com/data", { signal }) 
        .then((res) => res.json())
        ❌ მომხმარებელი გადადის სხვა გვერდზე
        .then((result) => setData(result)) 
        .catch((err) => {
            if (err.name === "AbortError") {
                console.log("ინფორმაციის წამოღების ოპერაცია გაუქმდა 🚫"); 
            } else {
                console.error(err);
            }
        });

    return () => controller.abort(); // ✅ გასუფთავება: fetch-ის გაუქმება კომპონენტის დემონტაჟისას
}, []);                   
                

signal არის AbortController-ის სიგნალი, რომელიც fetch() მეთოდს ეუბნება თუ როდის შეწყვიტოს მუშაობა. ამ სიგნალის მიღებისას fetch() აგენერირებს AbortError შეცდომას, რომელსაც ჩვენ .catch() ბლოკში ვამუშავებთ.

შეიძლება გაჩნდეს კითხები : რა ხდება გამსუფთავებელთან მიმართებაში ? მომხმარებელი ხომ გამსუფთავებლის შესრულებამდე გადავიდა სხვა გვერდზე ?

ამის შესახებ ოდნავ ზემოთაც ვისაუბრეთ - რეაქტი, კომპონენტის დემონტაჟამდე, ჯერ გამსუფთავებელს ეძებს, თუ იგი აღწერილია ასრულებს მის ინსტრუქციებს და მხოლდ ამის შემდეგ ახდენს კომპონენტის დემონტაჟს.

შევაჯამოთ ყველაფერი და კიდევ ერთხელ ვთქვათ თუ რა დანიშნულება აქვს useEffect ჰუკს. დავუბრუნდეთ დასაწყისში მოყვანილ მაგალითს და ჩავწეროთ იგი ჰუკის გარეშე :

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

    fetch("https://jsonplaceholder.typicode.com/posts")
        .then((response) => response.json())
        .then((json) => setData(json)); // ❌ გაეშვება ყოველი გენერირებისას 

    return (
        <div>
            <h1>სიახლეები</h1>
            <ul>
                {data.map((post) => (
                    <i key={post.id}>{post.title}</li>
                ))}
            </ul>
        </div>
    );
}            
                
  • კომპონენტის ყოველი გენერირებისას გაეშვება fetch(), ანუ გაიგზავნება არასაჭირო მოთხოვნები.
  • setData(json)-ს ყოველ შესრულებაზე მოხდება კომპონენტის ხელახალი გენერირება, რაც გამოიწვევს უსასრულო ციკლს.

ამ ყველაფერს, რა თქმა უნდა, პრაქტიკულ მაგალითებშიც ვიხილავთ.

14. ინფორმაციის მიღება სერვერიდან

სერვერის მოვალეობას შეგვისრულებს საკმაოდ მოსახერხებელი ვებ-გვერდი - jsonplaceholder, რომელიც საშუალებას გვაძლევს მოვახდინოთ HTTP მოთხოვნების სიმულაციები.

არსებობს HTTP მოთხოვნის გაკეთების ორი ვარიანტი : fetch და axios.

რა არის fetch ?

fetch არის ჯავასკრიპტში ჩაშენებული ფუნქცია, რომელიც საშუალებას გვაძლევს გავაგზავნოთ HTTP მოთხოვნები. იგი აბრუნებს დაპირებას (promise) და აქვს ყველა თანამედროვე ბრაუზერის მხარდაჭერა.

დადებითი მხარეები
  • არ ესაჭიროება არანაირი დამატებითი ინსტალაცია - ჩაშენებულია ჯავასკრიპტში.
  • მხარს უჭერს ასინქრონულ ოპერაციებთან სამუშაო ისეთ ხელსაწყოებს როგორებიცაა async/await.
  • მუშაობს როგორც სამომხმარებლო, ასევე სერვერული მხარის ჯავასკრიპტშიც.
უარყოფითი მხარეები
  • შეუძლებელია მოთხოვნის ავტომატურად გაუქმება საჭიროების შემთხვევაში.
  • ესაჭიროება დამატებითი ნაბიჯები შეცდომების დასამუშავებლად.
  • არ აკეთებს პასუხად მოსული ინფორმაციის ავტომატურ ფორმატირებას (ესაჭიროება .json() მეთოდი).

რა არის axios ?

axios არის ჯავასკრიპტის ბიბლიოთეკა, რომელიც, ასევე HTTP მოთხოვნების გასაკეთებლად გამოიყენება.

დადებითი მხარეები
  • შეუძლია მოთხოვნის ავტომატურად გაუქმება AbortController-ის დახმარებით.
  • აკეთებს პასუხად მოსული ინფორმაციის ავტომატურ ფორმატირებას (არ ესაჭიროება .json() მეთოდი).
  • მარტივად ხდება შეცდომების დამუშავება.
  • მუშაობს როგორც სამომხმარებლო, ასევე სერვერული მხარის ჯავასკრიპტშიც.
უარყოფითი მხარეები
  • ესაჭიროება ინსტალაცია.
  • შედარებით დიდი და მოცულობითია ვიდრე fetch.

fetch vs axios

პატარა ზომის პროექტებში, სადაც მარტივი HTTP მოთხოვნების გაკეთება გვიწევს, fetch() მეთოდი სრულიად საკმარისია. შედარებით რთულ და კომპლექსურ სიტუაციებში კი, სადაც შეცდომების დამუშავებაც საჭიროა, აუცილებლობის შემთხვევაში მოთხოვნების გაუქმებაც და პასუხად მიღებული ინფორმაციის ავტომატური დაფორმატებაც - axios უფრო კარგი არჩევანია.

დავაინსტალიროთ axios :

npm install axios             
                

აქვე აღვნიშნოთ, რომ ასინქრონულ ოპერაციებთან ვიმუშავებთ დამატებითი განმარტებების გარეშე. საჭირო ინფორმაცია შეგიძლიათ მოიძიოთ ასინქრონული ჯავასკრიპტისადმი მიძღვნილ ამ სტატიაში.

გავაგზავნოთ GET ტიპის მოთხოვნა და წამოვიღოთ ინფორმაცია მომხმარებლების შესახებ. App.tsx :

import axios from "axios";
import { useEffect, useState } from "react";

interface User {
    id: number;
    name: string;
}

function App() {
    const [users, setUsers] = useState<User[]>([]);

    useEffect(() => {
        axios.get("https://jsonplaceholder.typicode.com/users").then((res) => {
            console.log(res.data);
            setUsers(res.data);
        });
    }, []); // გაიგზავნოს მოთხოვნა მხოლოდ პირველი ჩატვირთვისას

    return (
        <ul>
            {users.map((user) => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    );
}

export default App;           
                

res ცვლადის დასახელება, რა თქმა უნდა, პირობითია. თავად ცვლადი კი სერვერიდან მიღებულ პასუხს მოიცავს, რომელიც დაახლოებით ასე გამოიყურება :


სამომხმარებლო მხარის დეველოპერებს (frontend) ხშირად ესაჭიროებათ მიმდინარე HTTP მოთხოვნებისათვის თვალყურის დევნება. დავაჭიროთ F12 ღილაკს და გადავინაცვლოთ Network განყოფილებაში, სადაც შესაძლებელია მოთხოვნების შესახებ ინფორმაციის ნახვა : რომელ ბმულს მივაკითხეთ, რა იყო მოთხოვნის ტანი, რა მივიღეთ პასუხად და ა.შ :

შეცდომების დამუშავება

შეცდომების დამუშავება საკმაოდ მარტივად ხდება :

import axios from "axios";
import { useEffect, useState } from "react";

interface User {
    id: number;
    name: string;
}

function App() {
    const [users, setUsers] = useState<User[]>([]);
    const [error, setError] = useState("");

    useEffect(() => {
        axios
            .get("https://jsonplaceholder.typicode.com/usersx") // მივუთითოთ არასწორი მისამართი
            .then((res) => {
                console.log(res.data);
                setUsers(res.data);
            })
            .catch((err) => setError(err.message));
    }, []); // გაიგზავნოს მოთხოვნა მხოლოდ პირველი ჩატვირთვისას

    return (
        <>
            {error && <p className="text-danger">{error}</p>}
            <ul>
            {users.map((user) => (
                <li key={user.id}>{user.name}</li>
            ))}
            </ul>
        </>
    );
}

export default App;                           
                

ვფიქრობ ამ კოდში ყველაფერი გასაგებია 😍.

მოთხოვნის გაუქმება

import axios, { CanceledError } from "axios";
import { useEffect, useState } from "react";

interface User {
    id: number;
    name: string;
}

function App() {
    const [users, setUsers] = useState<User[]>([]);
    const [error, setError] = useState("");

    useEffect(() => {
        const controller = new AbortController();

        axios
            .get("https://jsonplaceholder.typicode.com/users", {
                signal: controller.signal,
            })
            .then((res) => {
                console.log(res.data);
                setUsers(res.data);
            })
            .catch((err) => {
                if (err instanceof CanceledError) return;
                setError(err.message);
            });

            return () => controller.abort(); // გამსუფთავებელი
    }, []); // გაიგზავნოს მოთხოვნა მხოლოდ პირველი ჩატვირთვისას

    return (
        <>
            {error && <p className="text-danger">{error}</p>}
            <ul>
                {users.map((user) => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        </>
    );
}

export default App;                                    
                

ჩატვირთვის მაჩვენებელი

როგორც ვიცით, სერვერიდან ინფორმაციის წამოღებას ესაჭიროება გარკვეული დრო. კარგი იქნება თუ სამომხმარებლო ინტერფეისში რამენაირად მივანიშნებთ, რომ მიმდინარეობს ინფორმაციის ჩატვირთვა.

import axios, { CanceledError } from "axios";
import { useEffect, useState } from "react";

interface User {
    id: number;
    name: string;
}

function App() {
    const [users, setUsers] = useState<User[]>([]);
    const [error, setError] = useState("");
    const [isLoading, setLoading] = useState(false);

    useEffect(() => {
        const controller = new AbortController();
        setLoading(true);

        axios
            .get("https://jsonplaceholder.typicode.com/users", {
                signal: controller.signal,
            })
            .then((res) => {
                console.log(res.data);
                setUsers(res.data);
                setLoading(false);
            })
            .catch((err) => {
                if (err instanceof CanceledError) return;
                setError(err.message);
                setLoading(false);
            });

        return () => controller.abort(); // გამსუფთავებელი
    }, []); // გაიგზავნოს მოთხოვნა მხოლოდ პირველი ჩატვირთვისას

    return (
        <>
            {isLoading && 'იტვირთება ...'}
            {error && <p className="text-danger">{error}</p>}
            <ul>
                {users.map((user) => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        </>
    );
}

export default App;                                                        
                
15. ინფორმაციის წაშლა სერვერზე

App.tsx :

import axios, { CanceledError } from "axios";
import { useEffect, useState } from "react";

interface User {
    id: number;
    name: string;
}

function App() {
    const [users, setUsers] = useState<User[]>([]);
    const [error, setError] = useState("");
    const [isLoading, setLoading] = useState(false);

    useEffect(() => {
        const controller = new AbortController();
        setLoading(true);

        axios
            .get("https://jsonplaceholder.typicode.com/users", {
                signal: controller.signal,
            })
            .then((res) => {
                console.log(res.data);
                setUsers(res.data);
                setLoading(false);
            })
            .catch((err) => {
                if (err instanceof CanceledError) return;
                setError(err.message);
                setLoading(false);
            });

        return () => controller.abort(); // გამსუფთავებელი
    }, []); // გაიგზავნოს მოთხოვნა მხოლოდ პირველი ჩატვირთვისას

    const deleteUser = (user: User) => {
        const originalUsers = [...users];
        setUsers(users.filter((u) => u.id !== user.id));

        axios
            .delete("https://jsonplaceholder.typicode.com/users/" + user.id)
            .catch((err) => {
                setError(err.message);
                setUsers(originalUsers);
            });
    };

    return (
        <>
            {isLoading && "იტვირთება ..."}
            {error && <p className="text-danger">{error}</p>}
            <ul className="list-group">
            {users.map((user) => (
                <li
                    key={user.id}
                    className="list-group-item d-flex justify-content-between"
                    >
                    {user.name}
                    <button
                        className="btn btn-outline-danger"
                        onClick={() => deleteUser(user)}
                    >
                        წაშლა
                    </button>
                </li>
            ))}
            </ul>
        </>
    );
}

export default App;                                                                      
                

ვფიქრობ ამ კოდშიც გასაგებია ყველაფერი 😍, თუმცა აღნიშნვად ღირს, რომ სამომხმარებლო ინტერფეისის განახლების ამ სტილს ოპტიმისტური განახლება ეწოდება. როდესაც ინტერფეისის განახლება HTTP მოთხოვნის პასუხზეა დამოკიდებული, არსებობს ორი ვარიანტი : პირველი - განახლება მოხდეს პასუხის მიღებამდე და თუ სერვერზე რაიმე ხარვეზი დაფიქსირდება ინტერფეისი დაუბრუნდეს საწყის მდგომარეობას (ოპტიმისტური განახლება), და მეორე - ჯერ პასუხი დაბრუნდეს სერვერიდან და შემდეგ განახლდეს ინტერფეისი, ეს არის პესიმისტური განახლება. ეს უკანასკნელი შედარებით ნელია, მაგრამ უფრო საიმედო, რადგან განახლება მხოლოდ მაშინ ხდება, თუ HTTP მოთხოვნა წარმატებით სრულდება და შესაბამისად - უკან დასაბრუნებელიც არაფერი გვაქვს.

შეიძლება გაჩნდეს კითხვა : წაშლის მოთხოვნაც ხომ გვერდითი პროცესია, რატომ არ გამოვიყენეთ useEffect ჰუკი ? საქმე იმაშია, რომ ეს ჰუკი გამოიყენება ისეთ შემთხვევებში, როდესაც საჭიროა გვერდითი პროცესის ავტომატურად გაშვება ( მაგალითად სერვერიდან ინფორმაციის წამოღება კომპონენტის ჩატვირთვისას), წაშლის მოთხოვნა კი მომხმარებლის ქმედებაზეა დამოკიდებული - უნდა გაიგზავნოს მხოლოდ იმ შემთხვევაში, თუ მომხმარებელი წაშლის ღილაკს დააწვება.

16. ინფორმაციის დამატება სერვერზე

ინფორმაციის დამატებისასაც ოპტიმისტურ განახლებას მივმართოთ - ჯერ განვაახლოთ სამომხმარებლო ინტერფეისი და შემდეგ გავაგზავნოთ მოთხოვნა სერვერზე. App.tsx :

import axios, { CanceledError } from "axios";
import { useEffect, useState } from "react";

interface User {
    id: number;
    name: string;
}

function App() {
    const [users, setUsers] = useState<User[]>([]);
    const [error, setError] = useState("");
    const [isLoading, setLoading] = useState(false);

    useEffect(() => {
        const controller = new AbortController();
        setLoading(true);

        axios
            .get("https://jsonplaceholder.typicode.com/users", {
                signal: controller.signal,
            })
            .then((res) => {
                console.log(res.data);
                setUsers(res.data);
                setLoading(false);
            })
            .catch((err) => {
                if (err instanceof CanceledError) return;
                setError(err.message);
                setLoading(false);
            });

        return () => controller.abort(); // გამსუფთავებელი
    }, []); // გაიგზავნოს მოთხოვნა მხოლოდ პირველი ჩატვირთვისას

    const deleteUser = (user: User) => {
        const originalUsers = [...users];
        setUsers(users.filter((u) => u.id !== user.id));

        axios
            .delete("https://jsonplaceholder.typicode.com/users/" + user.id)
            .catch((err) => {
                setError(err.message);
                setUsers(originalUsers);
            });
    };

    const addUser = () => {
        const originalUsers = [...users];
        const newUser = { id: 0, name: "ვასო" };
        setUsers([newUser, ...users]);

        axios
            .post("https://jsonplaceholder.typicode.com/users", newUser)
            .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
            .catch((err) => {
                setError(err.message);
                setUsers(originalUsers);
            });
    };

    return (
        <>
            {isLoading && "იტვირთება ..."}
            {error && <p className="text-danger">{error}</p>}
            <button className="btn btn-primary" onClick={addUser}>
                დამატება
            </button>
            <ul className="list-group">
            {users.map((user) => (
                <li
                    key={user.id}
                    className="list-group-item d-flex justify-content-between"
                    >
                    {user.name}
                    <button
                        className="btn btn-outline-danger"
                        onClick={() => deleteUser(user)}
                    >
                        წაშლა
                    </button>
                </li>
            ))}
            </ul>
        </>
    );
}

export default App;                                                                                  
                
17. ინფორმაციის განახლება სერვერზე

App.tsx :

import axios, { CanceledError } from "axios";
import { useEffect, useState } from "react";

interface User {
    id: number;
    name: string;
}

function App() {
    const [users, setUsers] = useState<User[]>([]);
    const [error, setError] = useState("");
    const [isLoading, setLoading] = useState(false);

    useEffect(() => {
        const controller = new AbortController();
        setLoading(true);

        axios
            .get("https://jsonplaceholder.typicode.com/users", {
                signal: controller.signal,
            })
            .then((res) => {
                console.log(res.data);
                setUsers(res.data);
                setLoading(false);
            })
            .catch((err) => {
                if (err instanceof CanceledError) return;
                setError(err.message);
                setLoading(false);
            });

        return () => controller.abort(); // გამსუფთავებელი
    }, []); // გაიგზავნოს მოთხოვნა მხოლოდ პირველი ჩატვირთვისას

    const deleteUser = (user: User) => {
        const originalUsers = [...users];
        setUsers(users.filter((u) => u.id !== user.id));

        axios
            .delete("https://jsonplaceholder.typicode.com/users/" + user.id)
            .catch((err) => {
                setError(err.message);
                setUsers(originalUsers);
            });
    };

    const addUser = () => {
        const originalUsers = [...users];
        const newUser = { id: 0, name: "ვასო" };
        setUsers([newUser, ...users]);

        axios
            .post("https://jsonplaceholder.typicode.com/users", newUser)
            .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
            .catch((err) => {
                setError(err.message);
                setUsers(originalUsers);
            });
    };

    const updateUser = (user: User) => {
        const updatedUser = {...user, name: 'ახალი სახელი'};
        setUsers(users.map((u) => u.id === user.id ? updatedUser : u));
        const originalUsers = [...users];

        axios
            .patch("https://jsonplaceholder.typicode.com/users/" + user.id, updatedUser)
            .catch((err) => {
                setError(err.message);
                setUsers(originalUsers);
            });
    };

    return (
        <>
            {isLoading && "იტვირთება ..."}
            {error && <p className="text-danger">{error}</p>}
            <button className="btn btn-primary" onClick={addUser}>
            დამატება
            </button>
            <ul className="list-group">
                {users.map((user) => (
                    <li
                        key={user.id}
                            className="list-group-item d-flex justify-content-between"
                        >
                        {user.name}
                        <div>
                            <button
                                className="btn btn-secondary mx-1"
                                onClick={() => updateUser(user)}
                            >
                                რედაქტირება
                            </button>
                            <button
                                className="btn btn-outline-danger"
                                onClick={() => deleteUser(user)}
                            >
                                წაშლა
                            </button>
                        </div>
                    </li>
                ))}
            </ul>
        </>
    );
}

export default App;                                                                                   
                

კოდის დახვეწა

როგორც ვხედავთ, კოდში განმეორებადი ფრაგმენტები გვაქვს, მაგალითად jsonplaceholder-თან წვდომის საბაზისო წერტილი :

https://jsonplaceholder.typicode.com                                                                     
                

შევქმნათ ახალი საქაღალდე /src/services, მასში კი ფაილი api-client.ts შემდეგი კოდით :

import axios, { CanceledError } from "axios";

export default axios.create({
    baseURL: "https://jsonplaceholder.typicode.com",
});

export { CanceledError };                    
                

ახლა კი შევიტანოთ ცვლილებები App.tsx ფაილში :

import { useEffect, useState } from "react";
import apiClient, { CanceledError } from "./services/api-client";

interface User {
    id: number;
    name: string;
}

function App() {
    const [users, setUsers] = useState<User[]>([]);
    const [error, setError] = useState("");
    const [isLoading, setLoading] = useState(false);

    useEffect(() => {
        const controller = new AbortController();
        setLoading(true);

        apiClient
            .get("/users", {
                signal: controller.signal,
            })
            .then((res) => {
                console.log(res.data);
                setUsers(res.data);
                setLoading(false);
            })
            .catch((err) => {
                if (err instanceof CanceledError) return;
                setError(err.message);
                setLoading(false);
            });

        return () => controller.abort(); // გამსუფთავებელი
    }, []); // გაიგზავნოს მოთხოვნა მხოლოდ პირველი ჩატვირთვისას

    const deleteUser = (user: User) => {
        const originalUsers = [...users];
        setUsers(users.filter((u) => u.id !== user.id));

        apiClient.delete("/users/" + user.id).catch((err) => {
            setError(err.message);
            setUsers(originalUsers);
        });
    };

    const addUser = () => {
        const originalUsers = [...users];
        const newUser = { id: 0, name: "ვასო" };
        setUsers([newUser, ...users]);

        apiClient
            .post("/users", newUser)
            .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
            .catch((err) => {
                setError(err.message);
                setUsers(originalUsers);
            });
    };

    const updateUser = (user: User) => {
        const updatedUser = { ...user, name: "ახალი სახელი" };
        setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));
        const originalUsers = [...users];

        apiClient.patch("/users/" + user.id, updatedUser).catch((err) => {
            setError(err.message);
            setUsers(originalUsers);
        });
    };

    return (
        <>
            {isLoading && "იტვირთება ..."}
            {error && <p className="text-danger">{error}</p>}
            <button className="btn btn-primary" onClick={addUser}>
                დამატება
            </button>
            <ul className="list-group">
                {users.map((user) => (
                    <li
                        key={user.id}
                        className="list-group-item d-flex justify-content-between"
                    >
                        {user.name}
                        <div>
                            <button
                                className="btn btn-secondary mx-1"
                                onClick={() => updateUser(user)}
                            >
                                რედაქტირება
                            </button>
                            <button
                                className="btn btn-outline-danger"
                                onClick={() => deleteUser(user)}
                            >
                                წაშლა
                            </button>
                        </div>
                    </li>
                ))}
            </ul>
        </>
    );
}

export default App;                               
                

User სერვისი

შეიძლება მოხდეს ისე, რომ მომხმარებლებთან წვდომა სხვადასხვა კომპონენტებიდან დაგვჭირდეს. მოვათავსოთ საჭირო ფუნქციონალი ერთ სერვისში და მივწვდეთ მას ჩვენი აპლიკაციის ნებისმიერი წერტილიდან. /src/services საქაღალდეში შევქმნათ ფაილი user-service.ts შემდეგი კოდით :

import apiClient from "./api-client";

export interface User {
    id: number;
    name: string;
}

export class UserService {
    getAllUsers() {
        const controller = new AbortController();
        const request = apiClient.get<User[]>("/users", {
            signal: controller.signal,
        });

        return {
            request,
            cancel: () => controller.abort()
        };
    }

    deleteUser(id: number) {
        return apiClient.delete("/users/" + id);
    }
    
    addUser(user: User) {
        return apiClient.post("/users", user);
    }
    
    updateUser(user: User) {
        return apiClient.patch("/users/" + user.id, user);
    }
}

export default new UserService();                                  
                

ახლა კი შევიტანოთ ცვლილებები App.tsx ფაილში :

import { useEffect, useState } from "react";
import { CanceledError } from "./services/api-client";
import userService, { User } from "./services/user-service";

function App() {
    const [users, setUsers] = useState<User[]>([]);
    const [error, setError] = useState("");
    const [isLoading, setLoading] = useState(false);

    useEffect(() => {
        setLoading(true);
        const { request, cancel } = userService.getAllUsers();

        request
            .then((res) => {
                console.log(res.data);
                setUsers(res.data);
                setLoading(false);
            })
            .catch((err) => {
                if (err instanceof CanceledError) return;
                setError(err.message);
                setLoading(false);
            });

        return () => cancel(); // გამსუფთავებელი
    }, []); // გაიგზავნოს მოთხოვნა მხოლოდ პირველი ჩატვირთვისას

    const deleteUser = (user: User) => {
        const originalUsers = [...users];
        setUsers(users.filter((u) => u.id !== user.id));

        userService.deleteUser(user.id).catch((err) => {
            setError(err.message);
            setUsers(originalUsers);
        });
    };

    const addUser = () => {
        const originalUsers = [...users];
        const newUser = { id: 0, name: "ვასო" };
        setUsers([newUser, ...users]);

        userService
            .addUser(newUser)
            .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
                .catch((err) => {
                setError(err.message);
                setUsers(originalUsers);
            });
    };

    const updateUser = (user: User) => {
        const updatedUser = { ...user, name: "ახალი სახელი" };
        setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));
        const originalUsers = [...users];

        userService.updateUser(updatedUser).catch((err) => {
            setError(err.message);
            setUsers(originalUsers);
        });
    };

    return (
        <>
            {isLoading && "იტვირთება ..."}
            {error && <p className="text-danger">{error}</p>}
            <button className="btn btn-primary" onClick={addUser}>
                დამატება
            </button>
            <ul className="list-group">
                {users.map((user) => (
                    <li
                        key={user.id}
                        className="list-group-item d-flex justify-content-between"
                    >
                        {user.name}
                        <div>
                            <button
                                className="btn btn-secondary mx-1"
                                onClick={() => updateUser(user)}
                            >
                                რედაქტირება
                            </button>
                            <button
                                className="btn btn-outline-danger"
                                onClick={() => deleteUser(user)}
                            >
                                წაშლა
                            </button>
                        </div>
                    </li>
                ))}
            </ul>
        </>
    );
}

export default App;                    
                

ასე და ამგვარად : App.tsx ფაილშიც შედარებით მარტივი კოდი გვაქვს და მომხმარებლებთან სამუშაო ფუნქციონალიც მრავალჯერადად გამოყენებადია - UserService-ს გამოვიძახებთ ყველგან, სადაც დაგვჭირდება.

18. სამომხმარებლო სერვისის შექმნა

წინა თავში გავაკეთეთ სერვისი UserService , სადაც თავმოყრილია მომხმარებლებთან სამუშაო მეთოდები : სიის გამოტანა, დამატება, რედაქტირება, წაშლა. შესაძლებელია მომავალში დაგვჭირდეს სხვა ტიპის რესურსებთან სამუშაო, ზუსტად იგივე ტიპის მეთოდები (მაგ: სიახლეებთან, კომენტარებთან და ა.შ). მაგალითად jsonplaceholder-ს თუ გადავამოწმებთ, მას აქვს შემდეგი რესურსები :

/posts	
/comments	
/albums	
/photos	
/todos	
/users	
                

აღვწეროთ სერვისი, რომელსაც პარამეტრად გადაეცემა რესურსის მაჩვენებელი და რომელიც ამ პარამეტრის მიხედვით გააგზავნის HTTP მოთხოვნებს საჭირო მისამართებზე.

/src/services საქაღალდეში შევქმნათ ფაილი http-service.ts შემდეგი კოდით :

import apiClient from "./api-client";

/* 
    ეს ინტერფეისი გარანტიას გვაძლევს, რომ მეთოდებისათვის პარამეტრებად 
    გადაცემულ ობიექტებს აუცილებლად ექნებათ id ველი
*/
interface Entity {
    id: number;
}

export class HttpService {
    resource: string;

    constructor(resource: string) {
        this.resource = resource;
    }

    getAll() {
        const controller = new AbortController();
        const request = apiClient.get(this.resource, {
            signal: controller.signal,
        });

        return {
            request,
            cancel: () => controller.abort(),
        };
    }

    delete(id: number) {
        return apiClient.delete(this.resource + "/" + id);
    }

    add<T>(Entity: T) {
        return apiClient.post(this.resource, Entity);
    }

    /* 
        მაგალითად აქ უნდა ვიყოთ დარწმუნებულები, რომ T-ს გააჩნია 
        id ველი, სწორედ ამიტომ დავუმემკვიდრევეთ იგი Entity ინტერფეისს
    */
    update<T extends Entity>(entity: T) {
        return apiClient.patch(this.resource + "/" + entity.id, entity);
    }
}

const create = (resource: string) => new HttpService(resource);

export default create;                                 
                
რა არის T ?

T არის ზოგადი ტიპის (generic Type) სახელსივრცე, ფსევდონიმი, რომლის კონკრეტული მნიშვნელობაც ფუნქციის გამოძახებისას განისაზღვრება. მაგალითად თუ add() მეთოდს გამოვიძახებთ User ობიექტით, T იქნება User, თუ გამოვიძახებთ Post ობიექტით, T იქნება Post და ა.შ.

user-service.ts ფაილში შევიტანოთ შემდეგი კოდი :

import create from "./http-service";

// ამ ინტერფეისს ვიყენებთ App.tsx-ში
export interface User {
    id: number;
    name: string;
}

// შეიქმნება HttpService 'users' რესურსისათვის
export default create("/users");                             
                

App.tsx :

import { useEffect, useState } from "react";
import { CanceledError } from "./services/api-client";
import userService, { User } from "./services/user-service";

function App() {
    const [users, setUsers] = useState<User[]>([]);
    const [error, setError] = useState("");
    const [isLoading, setLoading] = useState(false);

    useEffect(() => {
        setLoading(true);
        const { request, cancel } = userService.getAll();

        request
            .then((res) => {
                setUsers(res.data);
                setLoading(false);
            })
            .catch((err) => {
                if (err instanceof CanceledError) return;
                setError(err.message);
                setLoading(false);
            });

        return () => cancel(); // გამსუფთავებელი
    }, []); // გაიგზავნოს მოთხოვნა მხოლოდ პირველი ჩატვირთვისას

    const deleteUser = (user: User) => {
        const originalUsers = [...users];
        setUsers(users.filter((u) => u.id !== user.id));

        userService.delete(user.id).catch((err) => {
            setError(err.message);
            setUsers(originalUsers);
        });
    };

    const addUser = () => {
        const originalUsers = [...users];
        const newUser = { id: 0, name: "ვასო" };
        setUsers([newUser, ...users]);

        userService
            .add(newUser)
            .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
            .catch((err) => {
                setError(err.message);
                setUsers(originalUsers);
            });
    };

    const updateUser = (user: User) => {
        const updatedUser = { ...user, name: "ახალი სახელი" };
        setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));
        const originalUsers = [...users];

        userService.update(updatedUser).catch((err) => {
            setError(err.message);
            setUsers(originalUsers);
        });
    };

    return (
        <>
            {isLoading && "იტვირთება ..."}
            {error && <p className="text-danger">{error}</p>}
            <button className="btn btn-primary" onClick={addUser}>
                დამატება
            </button>
            <ul className="list-group">
                {users.map((user) => (
                    <li
                        key={user.id}
                        className="list-group-item d-flex justify-content-between"
                    >
                    {user.name}
                    <div>
                        <button
                            className="btn btn-secondary mx-1"
                            onClick={() => updateUser(user)}
                        >
                            რედაქტირება
                        </button>
                        <button
                            className="btn btn-outline-danger"
                            onClick={() => deleteUser(user)}
                        >
                            წაშლა
                        </button>
                    </div>
                </li>
            ))}
            </ul>
        </>
    );
}

export default App;                                             
                
19. სამომხმარებლო ჰუკის შექმნა

ამჟამად მომხარებლების სია მხოლოდ App.tsx ფაილში გამოგვაქვს. შეიძლება მოხდეს ისე, რომ ეს სია სხვა კომპონენტშიც დაგვჭირდეს. ამიტომ გავიტანოთ მისი გენერირების ლოგიკა ცალკე. შევქმნათ საქაღალდე /src/hooks, მასში კი ფაილი useUsers.ts შემდეგი კოდით :

import { useEffect, useState } from "react";
import userService, { User } from "../services/user-service";
import { CanceledError } from "axios";

const useUsers = () => {
    const [users, setUsers] = useState<User[]>([]);
    const [error, setError] = useState("");
    const [isLoading, setLoading] = useState(false);

    useEffect(() => {
        setLoading(true);
        const { request, cancel } = userService.getAll();

        request
            .then((res) => {
                setUsers(res.data);
                setLoading(false);
            })
            .catch((err) => {
                if (err instanceof CanceledError) return;
                setError(err.message);
                setLoading(false);
            });

        return () => cancel(); // გამსუფთავებელი
    }, []); // გაიგზავნოს მოთხოვნა მხოლოდ პირველი ჩატვირთვისას

    return { users, setUsers, error, setError, isLoading };
};

export default useUsers;                                          
                

App.tsx :

import userService, { User } from "./services/user-service";
import useUsers from "./hooks/useUsers";

function App() {
    const { users, setUsers, error, setError, isLoading } = useUsers();

    const deleteUser = (user: User) => {
        const originalUsers = [...users];
        setUsers(users.filter((u) => u.id !== user.id));

        userService.delete(user.id).catch((err) => {
            setError(err.message);
            setUsers(originalUsers);
        });
    };

    const addUser = () => {
        const originalUsers = [...users];
        const newUser = { id: 0, name: "ვასო" };
        setUsers([newUser, ...users]);

        userService
            .add(newUser)
            .then(({ data: savedUser }) => setUsers([savedUser, ...users]))
            .catch((err) => {
                setError(err.message);
                setUsers(originalUsers);
            });
    };

    const updateUser = (user: User) => {
        const updatedUser = { ...user, name: "ახალი სახელი" };
        setUsers(users.map((u) => (u.id === user.id ? updatedUser : u)));
        const originalUsers = [...users];

        userService.update(updatedUser).catch((err) => {
            setError(err.message);
            setUsers(originalUsers);
        });
    };

    return (
        <>
            {isLoading && "იტვირთება ..."}
            {error && <p className="text-danger">{error}</p>}
            <button className="btn btn-primary" onClick={addUser}>
                დამატება
            </button>
            <ul className="list-group">
                {users.map((user) => (
                    <li
                        key={user.id}
                        className="list-group-item d-flex justify-content-between"
                    >
                    {user.name}
                    <div>
                        <button
                            className="btn btn-secondary mx-1"
                            onClick={() => updateUser(user)}
                        >
                            რედაქტირება
                        </button>
                        <button
                            className="btn btn-outline-danger"
                            onClick={() => deleteUser(user)}
                        >
                            წაშლა
                        </button>
                    </div>
                </li>
            ))}
            </ul>
        </>
    );
}

export default App;                                             
                
20. ეპილოგი

გილოცავთ ! 🍾 🍾 🍾

ჩვენ უკვე გავიარეთ კურსის თეორიული ნაწილი !

შევხვდებით პრაქტიკულ განყოფილებაში, სადაც გავაერთიანებთ ყველაფერს, რაც აქამდე ვისწავლეთ და შევქმნით ფილმების საძიებო სისტემას.

პრაქტიკა - ფილმების საძიებო სისტემა
1. რას გავაკეთებთ პრაქტიკულ განყოფილებაში ?

პრაქტიკულ განყოფილებაში შევქმნით ფილმების საძიებო სისტემას.

მზა კოდი შეგიძლიათ იხილოთ აქ. პროექტის გასაშვებად მიჰყევით ინსტრუქციას :

  1. ჩამოტვირთეთ პროექტი თქვენს ლოკალურ გარემოში.
  2. გაუშვით npm install ბრძანება.
  3. დარეგისტრირდით და დააგენერირეთ API key : Themoviedb (ავტორიზაციის შემდეგ : Settings -> Api).
  4. დაამატეთ API key src/services/api.ts ფაილში.
  5. გაუშვით npm run dev ბრძანება.

ეს რაც შეეხებოდა მზა კოდს, ხოლო თუ გსურთ თავად ააწყოთ პროექტი თავიდან ბოლომდე, შევხვდებით მე-2 თავში 😎 😍

2. პროექტის ინსტალაცია, საწყისი კომპონენტები, CSS სტილები

ვიდრე საქმეს შევუდგებით, მინდა აღვნიშნო - პრაქტიკულ განყოფილებაში მხოლოდ იმ საკითხებთან დაკავშირებით გავაკეთებთ განმარტებებს, რაზეც თეორიულ ნაწილში არ გვისაუბრია.

მაშ ასე, დავაინსტალიროთ ახალი პროექტი :

npm create vite@latest react-moviedb
                
cd react-moviedb
npm install
npm run dev
                

/src საქაღალდეში შევქმნათ შემდეგი საქაღალდეები :

components
css
pages
                

წავშალოთ /src საქაღალდეში არსებული App.css ფაილი, index.css ფაილი კი ახლადშექმნილ css საქაღალდეში გადავიტანოთ ამ კოდით :

:root {
    font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
    line-height: 1.5;
    font-weight: 400;

    color-scheme: light dark;
    color: rgba(255, 255, 255, 0.87);
    background-color: #242424;

    font-synthesis: none;
    text-rendering: optimizeLegibility;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html, body {
    overflow-x: hidden;
}

body {
    margin: 0;
    width: 100%;
    min-height: 100vh;
    position: relative;
}

#root {
    width: 100%;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

a {
    font-weight: 500;
    color: #646cff;
    text-decoration: inherit;
}

a:hover {
    color: #535bf2;
}

h1 {
    font-size: 3.2em;
    line-height: 1.1;
}

button {
    border-radius: 8px;
    border: 1px solid transparent;
    padding: 0.6em 1.2em;
    font-size: 1em;
    font-weight: 500;
    font-family: inherit;
    background-color: #1a1a1a;
    cursor: pointer;
    transition: border-color 0.25s;
}

button:hover {
    border-color: #646cff;
}

button:focus,
button:focus-visible {
    outline: 4px auto -webkit-focus-ring-color;
}

@media (prefers-color-scheme: light) {
    :root {
        color: #213547;
        background-color: #ffffff;
    }
    a:hover {
        color: #747bff;
    }
    button {
        background-color: #f9f9f9;
    }
}
                

პროექტის სტილები

კურსის მიზანს არ წარმოადგენს CSS-ის ცოდნის გაღრმავება, ამიტომ პირდაპირ შევქმნათ საჭირო ახალი ფაილები css საქაღალდეში :

Favorites.css :
.favorites {
    padding: 2rem;
    width: 100%;
    box-sizing: border-box;
    text-align: center;
}

.favorites h2 {
    margin-bottom: 2rem;
    text-align: center;
    font-size: 2.5rem;
}

.favorites-empty {
    text-align: center;
    padding: 4rem 2rem;
    background-color: rgba(255, 255, 255, 0.05);
    border-radius: 12px;
    margin: 2rem auto;
    max-width: 600px;
}
                      
.favorites-empty h3 {
    margin-bottom: 1rem;
    color: #e50914;
}

.favorites-empty p {
    color: #999;
    font-size: 1.2rem;
    line-height: 1.6;
}
                      
@media (max-width: 639px) {
    .favorites {
        padding: 2rem 0;
    }  

    .favorites h2 {    
        font-size: 1.5rem;
    }

    .movies-grid {
        display: flex;
        justify-content: center;
        align-items: center;
        flex-wrap: wrap;
        gap: 1rem; 
    }

    .movie-card {
        max-width: 100%; /* Ensure it doesn't take full width */
    }
}
                      
@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translateY(20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.movies-grid > * {
    animation: fadeIn 0.3s ease-out forwards;
}                    
                
Home.css :
.home {
    padding: 2rem 0;
    width: 100%;
    box-sizing: border-box;
}

.loading {
    width: 100%;
    text-align: center;
}

.search-form {
    max-width: 600px;
    margin: 0 auto 2rem;
    display: flex;
    padding: 0 1rem;
    box-sizing: border-box;
}

.search-input {
    flex: 1;
    padding: 0.75rem 1rem;
    border: none;
    background-color: #333;
    color: white;
    font-size: 1rem;
}

.search-input:focus {
    outline: none;
    box-shadow: 0 0 0 2px #666;
}


@media (max-width: 639px) {
    .home {
        padding: 1rem 0;
    }

    .search-form {
        margin-bottom: 1rem;
    }
}

.movies-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 1.5rem;
    padding: 1rem;
    width: 100%;
    box-sizing: border-box;
}                      
                
MovieCard.css :
.movie-card {
    position: relative;
    border-radius: 8px;
    overflow: hidden;
    background-color: #1a1a1a;
    transition: transform 0.2s;
    height: 100%;
    display: flex;
    flex-direction: column;
}

.movie-card:hover {
    transform: translateY(-5px);
}

.movie-poster {
    position: relative;
    aspect-ratio: 2/3;
    width: 100%;
}

.movie-poster img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.movie-overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: linear-gradient(
        to bottom,
        rgba(0, 0, 0, 0.1),
        rgba(0, 0, 0, 0.8)
    );
    opacity: 0;
    transition: opacity 0.2s;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding: 1rem;
}

.movie-card:hover .movie-overlay {
    opacity: 1;
}

.favorite-btn {
    position: absolute;
    top: 1rem;
    right: 1rem;
    color: white;
    font-size: 1.5rem;
    padding: 0.5rem;
    background-color: rgba(0, 0, 0, 0.5);
    border-radius: 50%;
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background-color 0.2s;
}

.favorite-btn:hover {
    background-color: rgba(0, 0, 0, 0.8);
}

.favorite-btn.active {
    color: #ff4757;
}

.rating-select {
    background-color: rgba(0, 0, 0, 0.7);
    color: white;
    border: none;
    padding: 0.5rem;
    border-radius: 4px;
    cursor: pointer;
    margin-top: 0.5rem;
}

.movie-info {
    padding: 1rem;
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.movie-info h3 {
    font-size: 1rem;
    margin: 0;
}

.movie-info p {
    color: #999;
    font-size: 0.9rem;
}

.user-rating {
    color: #ffd700;
    font-size: 0.9rem;
    margin-top: auto;
}

@media (max-width: 768px) {
    .movie-card {
        font-size: 0.9rem;
    }

    .movie-info {
        padding: 0.75rem;
    }

    .favorite-btn {
        width: 32px;
        height: 32px;
        font-size: 1.2rem;
    }
}
                
Navbar.css :
.navbar {
    background-color: #000000;
    padding: 1rem 2rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.navbar-brand {
    font-size: 1.5rem;
    font-weight: bold;
}

.navbar-links {
    display: flex;
    gap: 2rem;
}

.nav-link {
    font-size: 1rem;
    padding: 0.5rem 1rem;
    border-radius: 4px;
    transition: background-color 0.2s;
}

.nav-link:hover {
    background-color: rgba(255, 255, 255, 0.1);
}

@media (max-width: 768px) {
    .navbar {
        padding: 1rem;
    }

    .navbar-brand {
        font-size: 1.2rem;
    }

    .navbar-links {
        gap: 1rem;
    }

    .nav-link {
        padding: 0.5rem;
    }
}
                

კომპონენტები

/src/components საქაღალდეში შევქმნათ MovieCard.tsx ფაილი შემდეგი კოდით :

import "../css/MovieCard.css";

function MovieCard({ movie }: { movie: any }) {
    function onFavoriteClick() {
        alert();
    }

    return (
        <div className="movie-card">
            <div className="movie-poster">
                <img
                    src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
                    alt={movie.title}
                />
                <div className="movie-overlay">
                    <button className="favorite-btn" onClick={onFavoriteClick}>
                        ♥
                    </button>
                </div>
            </div>
            <div className="movie-info">
                <h3>{movie.title}</h3>
                <p>{movie.release_date}</p>
            </div>
        </div>
    );
}

export default MovieCard;                      
                

/src/pages საქაღალდეში შევქმნათ Home.tsx ფაილი შემდეგი კოდით :

import MovieCard from "../components/MovieCard";
import { FormEvent, useState } from "react";
import "../css/Home.css";

function Home() {
    const [searchQuery, setSearchQuery] = useState("");

    const movies = [
        { id: 1, title: "შინდლერის სია", release_date: "1993", poster_path: "" },
        { id: 2, title: "ტროა", release_date: "2004", poster_path: "" },
        { id: 3, title: "ცაცია", release_date: "2018", poster_path: "" },
    ];

    const handleSearch = async (e: FormEvent) => {
        e.preventDefault();
        alert(searchQuery);
        setSearchQuery("");
    };

    return (
        <div className="home">
            <form onSubmit={handleSearch} className="search-form">
                <input
                    type="text"
                    placeholder="მოძებნეთ ფილმი"
                    className="search-input"
                    value={searchQuery}
                    onChange={(e) => setSearchQuery(e.target.value)}
                />                
            </form>

            <div className="movies-grid">
                {movies.map((movie) => (
                    <MovieCard movie={movie} key={movie.id} />
                ))}
            </div>
        </div>
    );
}

export default Home;                                      
                

ახლა კი App.tsx ფაილში წავშალოთ ყველაფერი და შევიტანოთ ეს კოდი :

import Home from "./pages/Home";

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

export default App;                                                
                

ამ მომენტისათვის main.tsx ფაილი გამოიყურება ასე :

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
    <StrictMode>
        <App />
    </StrictMode>,
)                                                                 
                
3. მარშრუტიზატორი (react router)

რეაქტის მარშრუტიზატორი (react router) არის ბიბლიოთეკა, რომელიც საშუალებას გვაძლევს ვმართოთ ნავიგაციური მენიუები მრავალგვერდიან აპლიკაციებში, ვიმოძრაოთ სხვადასხვა კომპონენტებსა და გვერდებზე ბრაუზერში აპლიკაციის ხელახალი ჩტვირთვის (refresh) გარეშე. ბიბლიოთეკა მხარს უჭერს ბრაუზერის ისტორიასაც - შეგვიძლია გადავიდეთ წინა ან შემდეგ გვერდებზე (back/forward). შესალებელია მარშრუტებზე პარამეტრების მიმაგრებაც (მაგ. /movie/:id კონკრეტული ფილმის გვერდი).

მარშრუტიზატორის დასაინსტალირებლად გავუშვათ შემდეგი ბრძანება :

npm i react-router-dom                                     
                

საიტზე გვექნება ორი გვერდი : მთავარი გვერდი და ფავორიტი ფილმების გვერდი. /src/pages საქაღალდეში შევქმნათ კიდევ ერთი ფაილი Favorites.tsx შემდეგი კოდით :

import "../css/Favorites.css";

function Favorites() {
    return (
        <div className="favorites-empty">
            <h3>თქვენ არ გაქვთ ფავორიტი ფილმები</h3>
        </div>
    );
}
    
export default Favorites;                                         
                

მოვაქციოთ App კომპონენტი მარშრუტიზატორის კომპონენტში, main.tsx :

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./css/index.css";
import { BrowserRouter } from "react-router-dom";

createRoot(document.getElementById("root")!).render(
    <StrictMode>
        <BrowserRouter>
            <App />
        </BrowserRouter>
    </StrictMode>
);                                                        
                

App.tsx ფაილში კი შევიტანოთ ეს კოდი :

import Favorites from "./pages/Favorites";
import Home from "./pages/Home";
import { Routes, Route } from "react-router-dom";

function App() {
    return (
        <main className="main-content">
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/favorites" element={<Favorites />} />
            </Routes>
        </main>
    );
}

export default App;                                                               
                
path="/favorites" → განსაზღვრავს იმ URL-ს რომელზეც ეს მარშრუტი იქნება აქტიური.

element={<Favorites />} → ჩატვირთავს <Favorites /> კომპონენტს როდესაც URL იქნება /favorites.                                                
                

როგორც აღვნიშნეთ, გვექნება ორი გვერდი : მთავარი გვერდი და ფავორიტი ფილმების გვერდი. მარშრუტიზატორის დახმარებით ახლა უკვე შეგვიძლია მოვხვდეთ ამ გვერდებზე ამდაგვარი ბმულებით :

http://localhost:5173/ 
http://localhost:5173/favorites                               
                

ნავიგაციური მენიუ

/src/components საქაღალდეში შევქმნათ NavBar.tsx ფაილი შემდეგი კოდით :

import { Link } from "react-router-dom";
import "../css/Navbar.css";

function NavBar() {
    return (
        <nav className="navbar">
            <div className="navbar-brand">
                <Link to="/">MDB</Link>
            </div>
            <div className="navbar-links">
                <Link to="/" className="nav-link">
                    მთავარი
                </Link>
                <Link to="/favorites" className="nav-link">
                    ფავორიტები
                </Link>
            </div>
        </nav>
    );
}

export default NavBar;                                                                   
                

<Link to=""> კომპონენტი მუშაობს <a href=""></a> ელემენტის მსგავსად.

გამოვიყენოთ ნავიგაციური მენიუ App.tsx ფაილში და ასევე დავამატოთ ერთი მარშრუტიც, რომელიც შესაბამის შეტყობინებას გამოიტანს არარსებული გვერდების გახსნისას :

import NavBar from "./components/NavBar";
import Favorites from "./pages/Favorites";
import Home from "./pages/Home";
import { Routes, Route } from "react-router-dom";

function App() {
    return (
        <>
            <NavBar />
            <main className="main-content">
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route path="/favorites" element={<Favorites />} />
                    <Route path="*" element={<h2>404 - გვერდი ვერ მოიძებნა</h2>} />
                </Routes>
            </main>
        </>
    );
}

export default App;                                                                            
                

სურვილის შემთხვევაში შეგიძლიათ შექმნათ ახალი გვერდი /src/pages საქაღალდეში, მაგალითად NotFound.tsx და მიამაგროთ არარსებული გვერდების დამმუშავებელ მარშრუტს :

...
<Route path="*" element={<NotFound />} />
...
                
4. დაკავშირება ფილმების API-სთან

ფილმების მონაცემთა ბაზის მაგივრობას გაგვიწევს Themoviedb. საჭიროა მხოლოდ რეგისტრაცია და API გასაღების დაგენერირება. ავტორიზაციის შემდეგ გადაინაცვლეთ პარამეტრების გვერდზე :


შემდეგ კი გვერდითი მენიუდან გადადით API განყოფილებაში.

თუ რაიმე მიზეზების გამო ვერ დააგენერირებთ API გასაღებს, შეგიძლიათ გამოიყენოთ ეს გასაღები :

8872fb52023d01017835172a06cce8be
                

/src საქაღალდეში შევქმნათ კიდევ ერთი საქაღალდე services, მასში კი ფაილი api.ts შემდეგი კოდით :

const API_KEY = "აქ_ჩაწერეთ_დაგენერირებული_გასაღები";
const BASE_URL = "https://api.themoviedb.org/3";

export const getPopularMovies = async () => {
    const response = await fetch(`${BASE_URL}/movie/popular?api_key=${API_KEY}`);
    const data = await response.json();
    return data.results;
};

export const searchMovies = async (query: any) => {
    const response = await fetch(
        `${BASE_URL}/search/movie?api_key=${API_KEY}&query=${encodeURIComponent(
            query
        )}`
    );
    const data = await response.json();
    return data.results;
};
                

ამ კოდში ისეთი არაფერია, რაზეც აქამდე არ გვისაუბრია 😍

დავუბრუნდეთ Home.tsx ფაილს და შევიტანოთ მასში ცვლილებები, კერძოდ - ჩვენთვის უკვე კარგად ნაცნობი useEffect ჰუკის მეშვეობით წამოვიღოთ რეალური ინფორმაცია Themoviedb-დან :

import MovieCard from "../components/MovieCard";
import { FormEvent, useEffect, useState } from "react";
import "../css/Home.css";
import { getPopularMovies } from "../services/api";

interface Movie {
    id: number;
    title: string;
    release_date: string;
    poster_path: string;
}

function Home() {
    const [searchQuery, setSearchQuery] = useState("");
    const [movies, setMovies] = useState<Movie[]>([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState("");

    useEffect(() => {
        const loadPopularMovies = async () => {
            try {
                const popularMovies = await getPopularMovies();
                setMovies(popularMovies);
            } catch (err) {
                console.log(err);
                setError("ფილმების ჩატვირთვისას დაფიქსირდა შეცდომა...");
            } finally {
                setLoading(false);
            }
        };

        loadPopularMovies();
    }, []);

    const handleSearch = async (e: FormEvent) => {
        e.preventDefault();
        alert(searchQuery);
        setSearchQuery("");
    };

    return (
        <div className="home">
            <form onSubmit={handleSearch} className="search-form">
                <input
                    type="text"
                    placeholder="მოძებნეთ ფილმი"
                    className="search-input"
                    value={searchQuery}
                    onChange={(e) => setSearchQuery(e.target.value)}
                />                
            </form>

            {error && <div className="error-message">{error}</div>}

            {loading ? (
                <div className="loading">იტვირთება...</div>
            ) : (
                <div className="movies-grid">
                    {movies.map((movie) => (
                    <MovieCard movie={movie} key={movie.id} />
                    ))}
                </div>
            )}
        </div>
    );
}

export default Home;                    
                
5. ფილმების ძებნა

Home.tsx ფაილში აღწერილი handleSearch მეთოდი ამჟამად გამოიყურება ასე :

const handleSearch = async (e: FormEvent) => {
    e.preventDefault();
    alert(searchQuery);
    setSearchQuery("");
};
                

api.ts ფაილში აღწერილი searchMovies მეთოდი შევაიმპორტოთ Home.tsx ფაილში, შემდეგ კი გადავაკეთოთ handleSearch მეთოდი :

import { getPopularMovies, searchMovies } from "../services/api";

...

const handleSearch = async (e: FormEvent) => {
    e.preventDefault();
    if (!searchQuery.trim()) return;
    if (loading) return;

    setLoading(true);
    try {
        const searchResults = await searchMovies(searchQuery);
        setMovies(searchResults);
        setError("");
    } catch (err) {
        console.log(err);
        setError("ფილმების ძებნისას დაფიქსირდა შეცდომა...");
    } finally {
        setLoading(false);
    }
};
                
6. ფავორიტი ფილმები, useContext ჰუკი, ლოკალური საცავი (local storage)

useContext ჰუკი

დავუშვათ აპლიკაციაში გვაქვს კომპონენტების შემდეგი იერარქია: App → Profile → Contact და გვჭირდება, რომ მხოლოდ App და Contact კომპონენტებში გამოვიტანოთ მომხმარებლის ტელეფონის ნომერი :


არსებობს რამდენიმე ვარიანტი :

  1. სტატიკური ნომერი - ყველაზე ცუდ შემთხვევაში შეგვიძლია სტატიკურად ჩავსვათ ნომერი საჭირო კომპონენტებში, ყოველგვარი რეკვიზიტების (props) გარეშე.
  2. კომპონენტთა სრული იერარქია - ტელეფონის ნომერი რეკვიზიტის (props) სახით აღვწეროთ იერარქიულად ყველაზე ზემოთ მდგომ, ისეთ კომპონენტში, რომელშიც ნომერია საჭირო, ჩვენს შემთხვევაში - App კომპონენტში და რეკვიზიტს კომპონენტთა სრული იერარქია გავატაროთ საბოლოო დანიშნულებამდე :
    function App() {
        const phone = "+1 1234567890";
    
        // გამოვიყენოთ სადმე აქ
    
        ...
    
        // გადავცეთ Profile კომპონენტსაც
        return <Profile phone={phone} />;
    }
    
    function Profile({ phone }) {
        return <Contact phone={phone} />;
    }
    
    function Contact({ phone }) {
        return (
            <p>{phone}</p>
        );
    }
    
    export default App;                              
                            

    დამეთანხმებით, ასეთი მიდგომა საკმაოდ მოუხერხებელია, თანაც ზოგიერთ კომპონენტზე სრულიად არასაჭირო რეკვიზიტის მიმაგრება გვიწევს : მაგალითად Profile კომპონენტში საერთოდ არ ვიყენებთ ტელეფონის ნომერს - მას ეს რეკვიზიტი, მხოლოდ იმიტომ გადაეცა რომ მან, თავის მხრივ, შვილობილ Contact კომპონენტს გადასცეს.

    რაც უფრო დიდ იერარქიულ ხესთან გვექნება საქმე, მით უფრო მოუხერხებელი იქნება ეს მიდგომაც. მოკლედ - საკმაოდ დიდი 'ტვინის ბურღია' 🙈 და სწორედ ასეც უწოდებენ ხოლმე ამ პროცესს - 'prop drilling' ანუ რეკვიზიტთა 'ბურღვა' კომპონენტებში 😆

  3. მხოლოდ საჭირო კომპონენტებში - ტელეფონის ნომერი სათითაოდ აღვწეროთ იმ კომპონენტებში, რომლებშიც გვჭირდება :
    function App() {
        const phone = "+1 1234567890";
    
        // გამოვიყენოთ სადმე აქ
    
        ...
    
        return <Profile />;
    }
    
    function Profile() {
        return <Contact />;
    }
    
    function Contact() {
        const phone = "+1 1234567890";
    
        return (
            <p>{phone}</p>
        );
    }
    
    export default App;                              
                            

    ამ შემთხვევაში ბევრი 'ბურღვა' აღარ გვიწევს 😆 თუმცა ერთი და იგივეს კეთება გვიხდება სხვადასხვა ადგილას.

useContext ჰუკი საშუალებას გვაძლევს გლობალურად გავაზიაროთ ინფორმაცია კომპონენტებს შორის, ამ ინფორმაციის თითოეულ კომპონენტზე სათითაოდ მიმაგრების გარეშე. useContext ჰუკის გამოყენება ხელსაყრელია :

  • შაბლონის პარამეტრების მართვისას (მაგ. ნათელი რეჟიმი, მუქი რეჟიმი - dark mode, light mode).
  • მომხმარებელთა აუტენტიფიკაციის სისტემასთან მუშაობისას (ინფორმაცია აუტენტიფიცირებული მომხმარებლის შესახებ).
  • ენის პარამეტრების მართვისას (ინფორმაცია მიმდინარე ენის შესახებ მრავალენოვან აპლიკაციებში).
  • აპლიკაციისათვის საჭირო გლობალური ინფორმაციების მართვისას (მაგ. სამომხმარებლო კალათა ინტერნეტ-მაღაზიაში).
  • ...
***

როგორც ვიცით, MovieCard კომპონენტში გვაქვს ფილმის ფავორიტად მონიშვნის ღილაკი :


ღილაკს უნდა მივამაგროთ შესაბამისი ფუნქციონალი : თუ რომელიმე ფილმს მოვნიშნავთ ფავორიტად, მაშინ ეს ფილმი უნდა გამოჩნდეს ფავორიტი ფილმების გვერდზე, ხოლო თავად ღილაკს კი შეეცვალოს ფერი, რაც იმის მანიშნებელი იქნება, რომ ფილმი უკვე დამატებულია ფავორიტებში, ანუ ფავორიტი ფილმების შესახებ ინფორმაციას უნდა ვხედავდეთ ჩვენს სიტემაში არსებულ ორივე გვერდზე.

ინფორმაცია ფავორიტი ფილმების შესახებ შევინახოთ ლოკალურ საცავში, შემდეგ ეს ინფორმაცია, ანუ კონტექსტი გამოვიყენოთ იქ, სადაც დაგვჭირდება.

ლოკალური საცავი არის ბრაუზერში არსებული მექანიმი, რომელიც საშუალებას გვაძლევს შევინახოთ ინფორმაცია გვერდის ხელახალ ჩატვირთვებს (refresh) შორის. ინფორმაცია ინახება გასაღები-მნიშვნელობა წყვილების სახით და ხელმისაწვდომია მანამ, სანამ ჩვენვე არ წავშლით.

localStorage.setItem("key", "value");  // ინფორმაციის შენახვა
const value = localStorage.getItem("key");  // ინფორმაციის წაკითხვა
localStorage.removeItem("key");  // კონკრეტული ინფორმაციის წაშლა
localStorage.clear();  // სრულად გასუფთავება
                

/src საქაღალდეში შევქმნათ კიდევ ერთი საქაღალდე contexts, მასში კი ფაილი MovieContext.tsx შემდეგი კოდით :

import {
    createContext,
    useContext,
    useEffect,
    useState,
    ReactNode,
} from "react";

// აღვწეროთ ტიპი Movie 
interface Movie {
    id: number;
    title: string;
}

// აღვწეროთ ფავორიტი ფილმების კონტექსტის ტიპი
interface MovieContextType {
    favorites: Movie[];
    addToFavorites: (movie: Movie) => void;
    removeFromFavorites: (movieId: number) => void;
    isFavorite: (movieId: number) => boolean;
}

// აღვწეროთ კონტექსტის პროვაიდერის რეკვიზიტები
interface MovieProviderProps {
    children: ReactNode;
}

// შევქმნათ კონტექსტი და განვუსაზღვროთ ტიპი 
const MovieContext = createContext<MovieContextType | null>(null);

export const useMovieContext = () => {
    const context = useContext(MovieContext);
    if (!context) {
        throw new Error("useMovieContext უნდა გამოვიყენოთ მხოლოდ MovieProvider პროვაიდერში");
    }
    return context;
};

const MovieProvider: React.FC<MovieProviderProps> = ({ children }) => {
    const [favorites, setFavorites] = useState<Movie[]>([]);                   

    useEffect(() => {
        const storedFavs = localStorage.getItem("favorites");
        if (storedFavs) {
            try {
                setFavorites(JSON.parse(storedFavs) || []); // დავრწმუნდეთ რომ ყოველთვის მასივია
            } catch (error) {
                setFavorites([]); // ხარვეზის შემთხვევაში ფავორიტებად ჩავსვათ ცარიელი მასივი
            }
        }
    }, []);

    useEffect(() => {
        if (favorites.length > 0) {
            localStorage.setItem("favorites", JSON.stringify(favorites));
        }
        else{
            localStorage.removeItem("favorites")
        }
    }, [favorites]);    

    const addToFavorites = (movie: Movie) => {
        setFavorites((prev) => [...prev, movie]);
    };  

    const removeFromFavorites = (movieId: number) => {
        setFavorites((prev) => prev.filter((movie) => movie.id !== movieId));
    };  

    const isFavorite = (movieId: number): boolean => {
        return favorites.some((movie) => movie.id === movieId);
    };

    const value: MovieContextType = {
        favorites,
        addToFavorites,
        removeFromFavorites,
        isFavorite,
    };

    return (
        <MovieContext.Provider value={value}>{children}</MovieContext.Provider>
    );
};

export default MovieProvider;                                            
                

განვიხილოთ კოდის საკვანძო წერტილები 🧐

ინტერფეისები
Movie ტიპი
interface Movie {
    id: number;
    title: string;
}
                
  • განსაზღვრავს ფილმის მინიმალურ სტრუქტურას, თითოეულ ფილმს უნდა გააჩნდეს მინიმუმ ეს ორი ატრიბუტი:
    • id (უნიკალური რიცხვითი იდენტიფიკატორი).
    • title (დასახელება).
  • ეს გვაძლევს გარანტიას, რომ ვიმუშავებთ ფილმების სწორად სტრუქტურირებულ ობიექტებთან.
MovieContextType ტიპი
interface MovieContextType {
    favorites: Movie[]; 
    addToFavorites: (movie: Movie) => void;
    removeFromFavorites: (movieId: number) => void;
    isFavorite: (movieId: number) => boolean;
}
                

განსაზღვრავს თუ რას უზრუნველყოფს კონტექსტი, ანუ რას გავაზიარებთ გვერდებსა და კომპონენტებს შორის კონტექსტის დახმარებით :

  • favorites: ფავორიტი ფილმების მასივი.
  • addToFavorites(movie): ფილმის ფავორიტებში დამატების ფუნქცია.
  • removeFromFavorites(movieId): ფილმის ფავორიტებიდან წაშლის ფუნქცია.
  • isFavorite(movieId): ფუნქცია, რომელიც ამოწმებს არის თუ არა კონკრეტული ფილმი ფავორიტებში.
MovieProviderProps
interface MovieProviderProps {
    children: ReactNode;
}
                

MovieProviderProps ინტერფეისი განსაზღვრავს თუ რა ტიპის უნდა იყოს MovieProvider პროვაიდერის შვილობილი ელემენტები.

  • MovieProvider პროვაიდერი, რომელსაც ოდნავ ქვემოთ აღვწერთ, არის ერთგვარი გარსაკრი, სივრცე, რომელშიც უნდა მოვაქციოთ ის კომპონენტები, სადაც კონტექსტის გამოყენება გვსურს. ანუ პროვაიდერს შეიძლება გააჩნდეს შვილობილი კომპონენტები.
  • ReactNode ნიშნავს რომ პროვაიდერის შვილობილად შეიძლება ჩაჯდეს ნებისმიერი ვალიდური JSX კოდი.
კონტექსტის შექმნა
const MovieContext = createContext<MovieContextType | null>(null);
                
  • იქმნება რეაქტის კონტექსტი სახელად MovieContext.
  • კონტექსტში ინახება ფავორიტი ფილმები და საჭირო ფუნქციები.
სამომხარებლო ჰუკი (useMovieContext)
export const useMovieContext = () => {
    const context = useContext(MovieContext);
    if (!context) {
        throw new Error("useMovieContext უნდა გამოვიყენოთ მხოლოდ MovieProvider პროვაიდერში");
    }
    return context;
};
                
  • ეს არის სამომხმარებლო ჰუკი, რომელიც გვიმარტივებს კომპონენტებში კონტექსტის გამოყენებას.
  • ზემოთ აღვნიშნეთ, რომ ის სივრცე, რომელშიც ფილმების კონტექსტის გამოყენება გვსურს MovieProvider პროვაიდერში უნდა მოექცეს. თუ კონტექსტს მის გარეთ გამოვიყენებთ, დაგენერირდება შეცდომა. ეს კი საშუალებას მოგვცემს გავაკონტროლოთ კონტექსტთან წვდომის წერტილები.
პროვაიდერის შექმნა (MovieProvider)
const MovieProvider: React.FC<MovieProviderProps> = ({ children }) => {
                
  • განისაზღვრა პროვაიდერი, რომელიც უნდა შემოერტყას აპლიკაციის იმ ნაწილს, რომელშიც კონტექსტის გამოყენება გვსურს.
  • მას რეკვიზიტად (props) გადაეცემა children, რაც ნიშნავს, რომ პროვაიდერის შვილობილად ნებისმიერი კომპონენტის ჩასმა შეგვიძლია (ReactNode).
კომპონენტის მდგომარეობის მართვა
const [favorites, setFavorites] = useState<Movie[]>([]);                   
                
  • favorites არის მასივი, რომელშიც ინახება მომხმარებლის ფავორიტი ფილმები.
  • setFavorites ანახლებს ფავორიტი ფილმების სიას.
  • დასაწყისში სია ცარიელია ([]), ლოკალური საცავიდან შევავსებთ ოდნავ მოგვიანებით.
ფავორიტი ფილმების ამოღება ლოკალური საცავიდან
useEffect(() => {
    const storedFavs = localStorage.getItem("favorites");
    if (storedFavs) {
        try {
            setFavorites(JSON.parse(storedFavs) || []); // დავრწმუნდეთ რომ ყოველთვის მასივია
        } catch (error) {
            setFavorites([]); // ხარვეზის შემთხვევაში ფავორიტებად ჩავსვათ ცარიელი მასივი
        }
    }
}, []);                   
                
  • ლოკალური საცავიდან ფავორიტი ფილმების ამოღების პროცესი არის გვერდითი პროცესი, რომელიც კომპონენტის თავდაპირველი ჩატვირთვისას უნდა გაეშვას (სწორედ ამიტომაა გადაცემული დამოკიდებულებათა ცარიელი მასივი).
  • მოხდა საცავში არსებული ფავორიტი ფილმების წაკითხვა
  • მოხდა წაკითხული ინფორმაციის მასივად დაფორმატება (JSON.parse())
  • თუ რაიმე შეცდომა დაფიქსირდება, ფავორიტ ფილმებად ჩაჯდება ცარიელი მასივი.
ფავორიტი ფილმების შენახვა ლოკალურ საცავში
useEffect(() => {
    if (favorites.length > 0) {
        localStorage.setItem("favorites", JSON.stringify(favorites));
    }
    else{
        localStorage.removeItem("favorites")
    }
}, [favorites]);         
                
  • ამჯერად ჰუკი გაეშვება ყოველ ჯერზე როცა კი დამოკიდებულების მნიშვნელობა შეიცვლება - ფავორიტებში დაემატება ან წაიშლება ფილმი.
  • თუ ფავორიტებში ერთი ფილმი მაინცაა, მაშინ ფავორიტების მასივი შეინახება ლოკალურ საცავში.
  • თუ ფავორიტები გაცარიელდება, მაშინ ლოკალური საცავიდან წაიშლება შესაბამისი გასაღები.
ფავორიტი ფილმების სამართავი ფუნქციები
ფილმის დამატება ფავორიტებში
const addToFavorites = (movie: Movie) => {
    setFavorites((prev) => [...prev, movie]);
};     
                
  • setFavorites() მეთოდს არგუმენტად გადაეცემა ისარ-ფუნქცია, რომელიც იღებს prev ცვლადს და აბრუნებს განახლებულ მდგომარეობას.
  • prev ცვლადი წარმოადგენს ფავორიტი ფილმების მდომარეობას მიმდინარე განახლებამდე.

რა საჭიროა prev ცვლადი ? ჩვენ ხომ უკვე გვაქვს ფავორიტი ფილმები შენახული :

const [favorites, setFavorites] = useState<Movie[]>([]);                   
                

მაშ რატომ არ ვიქცევით ასე ? :

setFavorites([...favorites, movie]);    
                

საქმე იმაშია, რომ თუ ასე მოვიქცევით 'favorites'-ში ყოველთვის ის მდგომარეობა გვექნება რაც კომპონენტის ბოლო ჩატვირთვისას იყო.

ახლა კი მოვლენათა ასეთი განვითარება წარმოვიდგინოთ :

  1. favorites = [A]
  2. setFavorites([...favorites, B]) (→ ახალი მდგომარეობა [A, B])
  3. ბუნებრივია, მდგომარეობის შეცვლისას რეაქტი კომპონენტის ხელახალ გენერირებას (re-render) მოახდენს, მაგრამ რა მოხდება თუ ამ ხელახალი გენერირების დასრულებამდე კიდევ ერთ ფილმს დავამატებთ ფავორიტებში ? :
    setFavorites([...favorites, C]); // 'favorites' არის ჯერ ისევ '[A]'
                            
  4. საბოლოო მდგომარეობა: [A, C] (B დაიკარგა 😲)

სწორედ ამიტომაა საჭირო შუამავალი ცვლადი prev, იგი გარანტიას გვაძლევს, რომ რეაქტი ყოველთვის მიმდინარე მდგომარეობას გამოიყენებს და არა იმას, რაც კომპონენტის ბოლო ჩატვირთვისას გვქონდა.

ფილმის წაშლა ფავორიტებიდან
const removeFromFavorites = (movieId: number) => {
    setFavorites((prev) => prev.filter((movie) => movie.id !== movieId));
};    
                
გადამოწმება არის თუ არა ფილმი ფავორიტებში
const isFavorite = (movieId: number): boolean => {
    return favorites.some((movie) => movie.id === movieId); // true / false
};  
                

ვფიქრობ აქ ყველაფერი გასაგებია.

კონტექსტის მნიშვნელობის განსაზღვრა
const value: MovieContextType = {
    favorites,
    addToFavorites,
    removeFromFavorites,
    isFavorite,
};
                

შეიქმნა ობიექტი, რომელიც მოიცავს ფავორიტ ფილმებთან დაკავშირებულ ყველა საჭირო ინფორმაციასა და მეთოდს.

საბოლოო პროვაიდერი
return (
    <MovieContext.Provider value={value}>{children}</MovieContext.Provider>
);
                

პროვაიდერში მოქცეულ ნებისმიერ შვილობილ კომპონენტში გვექნება შესაძლებლობა მივწვდეთ ფავორიტ ფილმებს და მათთან სამუშაო ფუნქციებს.

კონტექსტის გამოყენება

გამოვიყენოთ შექმნილი კონტექსტი, App.tsx :

import Favorites from "./pages/Favorites";
import Home from "./pages/Home";
import { Routes, Route } from "react-router-dom";
import MovieProvider from "./contexts/MovieContext";
import NavBar from "./components/NavBar";

function App() {
    return (
        <MovieProvider>
            <NavBar />
            <main className="main-content">
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route path="/favorites" element={<Favorites />} />
                    <Route path="*" element={<h2>404 - გვერდი ვერ მოიძებნა</h2>} />
                </Routes>
            </main>
        </MovieProvider>
    );
}

export default App;
                

MovieCard.tsx ფაილს მივცეთ ასეთი სახე:

import { FormEvent } from "react";
import "../css/MovieCard.css";
import { useMovieContext } from "../contexts/MovieContext";

function MovieCard({ movie }: { movie: any }) {
    const { isFavorite, addToFavorites, removeFromFavorites } = useMovieContext();
    const favorite = isFavorite(movie.id);

    function onFavoriteClick(e: FormEvent) {
        e.preventDefault();
        if (favorite) removeFromFavorites(movie.id);
        else addToFavorites(movie);
    }

    return (
        <div className="movie-card">
            <div className="movie-poster">
                <img
                    src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
                    alt={movie.title}
                />
                <div className="movie-overlay">
                    <button
                        className={`favorite-btn ${favorite ? "active" : ""}`}
                        onClick={onFavoriteClick}
                    >
                    ♥
                    </button>
                </div>
            </div>
            <div className="movie-info">
                <h3>{movie.title}</h3>
                <p>{movie.release_date?.split("-")[0]}</p>
            </div>
        </div>
    );
}

export default MovieCard;                               
                

ვფიქრობ აქაც ყველაფერი გასაგებია ✊

დაბოლოს, გამოვიტანოთ ფავორიტი ფილმები შესაბამის გვერდზე. Favorites.tsx ფაილი:

import "../css/Favorites.css";
import { useMovieContext } from "../contexts/MovieContext";
import MovieCard from "../components/MovieCard";

function Favorites() {
    const { favorites } = useMovieContext();

    if (favorites && favorites.length > 0) {
        return (
            <div className="favorites">
                <h2>ფავორიტი ფილმები</h2>
                <div className="movies-grid">
                    {favorites.map((movie) => (
                        <MovieCard movie={movie} key={movie.id} />
                    ))}
                </div>
            </div>
        );
    }

    return (
        <div className="favorites-empty">
            <h3>თქვენ არ გაქვთ ფავორიტი ფილმები</h3>
        </div>
    );
}

export default Favorites;                    
                
7. პროექტის ატვირთვა სერვერზე

ადრე უკვე ვისაუბრეთ, რომ პროექტის რეალურ გარემოში განთავსებამდე საჭიროა მისი 'მომზადება', ერთგვარი გარდაქმნა. ამისათვის უნდა გავუშვათ შემდეგი ბრძანება :

npm run build
                

სწორედ ამ ბრძანების შედეგად შექმნილი dist საქაღალდე იტვირთება ხოლმე სერვერზე 😍

8. ეპილოგი

გილოცავთ ! ჩვენ გავიარეთ რეაქტის საბაზისო კურსი ! 🍾 🍾 🍾

შეიძლება ითქვას, რომ თქვენ უკვე ფლობთ რეაქტის საფუძვლებს 😍

კითხვებისა და გაუგებრობების შემთხვევაში გთხოვთ დატოვოთ კომენტარი 💖

გისურვებთ წარმატებას ! 💖