Αντικείμενα API, καταστήματα, useState, useEffect και άλλα…

Πριν από μερικά χρόνια έγραψα ένα άρθρο που περιγράφει πώς να δημιουργήσετε μια εφαρμογή τριών επιπέδων χρησιμοποιώντας το React. Αυτή η ανάρτηση δείχνει πώς να δημιουργήσετε την ίδια αρχιτεκτονική χρησιμοποιώντας τα React Hooks.

Τα επίμαχα επίπεδα είναι:

  • Το επίπεδο διεπαφής χρήστη
  • Το επιχειρηματικό επίπεδο
  • Το επίπεδο πρόσβασης δεδομένων

Όλα αυτά τα επίπεδα θα εμφανίζονται σε μια εφαρμογή που διαχειρίζεται μια λίστα εργασιών. Ο χρήστης μπορεί να δει όλα τα todos και επίσης να χρησιμοποιήσει μια αναζήτηση για να τα βρει.

Ας ξεκινήσουμε τη δημιουργία της εφαρμογής και ας ορίσουμε τα τρία επίπεδα.

Επίπεδο πρόσβασης δεδομένων

Το επίπεδο πρόσβασης δεδομένων διαχειρίζεται την επικοινωνία με το API υποστήριξης. Είναι κατασκευασμένο από αντικείμενα API που εκθέτουν λειτουργίες που κάνουν την επικοινωνία του backend.

Το αντικείμενο todoAPI ασχολείται με κλήσεις API λήψη και αλλαγή εργασιών. Εδώ είναι πώς μπορεί να μοιάζει.

const url = "https://jsonplaceholder.typicode.com/todos";
function toJson(response) {
  return response.json();
}
function get() {
  return fetch(url).then(toJson);
}
const api = Object.freeze({
  get
});
export default api;

Η συνάρτηση βοηθητικού προγράμματος get επιστρέφει όλες τις διαθέσιμες εργασίες.

Το αντικείμενο userAPI διαχειρίζεται την επικοινωνία με το User REST API.

const url = "https://jsonplaceholder.typicode.com/users";
function get() {
  return fetch(url).then(toJson);
}
function toJson(response) {
  return response.json();
}
const api = Object.freeze({
  get
});
export default api;

Και τα δύο αντικείμενα API επιστρέφουν υποσχέσεις.

Μια υπόσχεση είναι μια αναφορά σε ένα μελλοντικό αποτέλεσμα. Παρέχει πρόσβαση στο αποτέλεσμα μιας ασύγχρονης κλήσης.

Επιχειρησιακό Επίπεδο

Το επιχειρηματικό στρώμα αποτελείται κυρίως από καταστήματα.

Ένα κατάστημα είναι ένα αντικείμενο που διαχειρίζεται την κατάσταση που σχετίζεται με μια συγκεκριμένη οντότητα. Για παράδειγμα, το todoStore διαχειρίζεται τη λίστα των εργασιών που ανακτήθηκαν από το backend.

Todo Store

Μπορούμε απλά να δημιουργήσουμε ένα κατάστημα χρησιμοποιώντας τα άγκιστρα React. Η συνάρτηση που δημιουργεί το κατάστημα useTodoStore είναι ένα προσαρμοσμένο άγκιστρο. Χρησιμοποιεί το άγκιστρο useState για να ορίσει την κατάσταση που αποθηκεύει όλα τα todos που ανακτώνται από το backend.

const [todos, setTodos] = useState([]);

Όταν δημιουργείται το κατάστημα, πραγματοποιεί κλήση στο REST API χρησιμοποιώντας τη συνάρτηση βοηθητικού προγράμματος API που έχουμε ήδη δημιουργήσει και στη συνέχεια ενημερώνει την κατάσταση. Το άγκιστρο useEffect μας επιτρέπει να καλέσουμε μια επανάκληση μία φορά κατά την απόδοση του στοιχείου. Το useEffect παίρνει έναν πίνακα εξαρτήσεων. Όταν αυτός ο πίνακας είναι άδειος, η επανάκληση καλείται μόνο μία φορά.

useEffect(() => {
  api.get().then((data) => setTodos(data));
}, []);

Εδώ είναι το πλήρες προσαρμοσμένο άγκιστρο που δημιουργεί το κατάστημα Todo.

import { useState, useEffect } from "react";
import api from "./todoApi";
function useTodoStore() {
  const [todos, setTodos] = useState([]);
  useEffect(() => {
    api.get().then((data) => setTodos(data));
  }, []);
  return [todos];
}
export default useTodoStore;

Το κατάστημα Todo θα πρέπει επίσης να επιτρέπει την αναζήτηση για ένα todo. Μπορούμε να το πετύχουμε αυτό εκθέτοντας μια μέθοδο που ορίζει το τρέχον ερώτημα.

import { useState, useEffect } from "react";
import api from "./todoApi";
function useTodoStore() {
  const [todos, setTodos] = useState([]);
  const [query, setQuery] = useState({ text: "" });
  useEffect(() => {
    api.get().then((data) => setTodos(data));
  }, []);
  return [todos, setQuery];
}
export default useTodoStore;

Μπορούμε να ορίσουμε το ερώτημα, αλλά τα δεδομένα εκτεθειμένης κατάστασης εξακολουθούν να παρέχουν όλες τις ανακτηθείσες εργασίες από το backend. Πρέπει να εκθέσουμε έναν νέο πίνακα που δίνει τις φιλτραρισμένες εργασίες με βάση το τρέχον ερώτημα.

Η ακόλουθη συνάρτηση filterBy παίρνει μια λίστα εργασιών και ένα αντικείμενο ερωτήματος και επιστρέφει μια φιλτραρισμένη λίστα.

function by(query) {
  return function (todo) {
    return query?.text ? todo.title.includes(query.text) : true;
  };
}
function filterBy(todos, query) {
  const top = 25;
  return todos.filter(by(query)).slice(0, top);
}

Μπορούμε τώρα να χρησιμοποιήσουμε τη συνάρτηση βοηθητικού προγράμματος filterBy για να υπολογίσουμε το νέο filteredTodos.

import { useState, useEffect } from "react";
import api from "./todoApi";
function by(query) {}
function filterBy(todos, query) {}
function useTodoStore() {
  const [todos, setTodos] = useState([]);
  const [query, setQuery] = useState({ text: "" });
  useEffect(() => {
    api.get().then((data) => setTodos(data));
  }, []);
  const filteredTodos = filterBy(todos, query);
  return [filteredTodos, setQuery];
}
export default useTodoStore;

Κατάστημα χρηστών

Το κατάστημα χρήστη διαχειρίζεται τη λίστα των χρηστών.

Όταν δημιουργείται το κατάστημα, ανακτά όλους τους χρήστες από το backend χρησιμοποιώντας τη συνάρτηση βοηθητικού προγράμματος API που έχει ήδη δημιουργηθεί. Στη συνέχεια, η λίστα με όλους τους χρήστες διατηρείται στο κατάστημα.

import { useState, useEffect } from "react";
import api from "./userApi";
function useUserStore() {
  const [users, setUsers] = useState([]);
  useEffect(() => {
    api.get().then((data) => setUsers(data));
  }, []);
  return [users];
}
export default useUserStore;

Επίπεδο διεπαφής χρήστη

Το επίπεδο διεπαφής χρήστη αποδίδει τα δεδομένα στην οθόνη και χειρίζεται την είσοδο του χρήστη. Τα δεδομένα που εμφανίζονται στην οθόνη είναι αυτά που φυλάσσονται στα καταστήματα. Χειρίζεται τις ενέργειες των χρηστών καλώντας μεθόδους που αλλάζουν τα δεδομένα στα καταστήματα.

Είδος

Το στοιχείο ListItem εκτελεί ένα μεμονωμένο έργο και το εμφανίζει στη λίστα.

function ListItem({ title, userName }) {
  return (
    <li>
      <div>{title}</div>
      <div>{userName}</div>
    </li>
  );
}
export default ListItem;

Λίστα

Το στοιχείο List παίρνει μια λίστα εργασιών και την εμφανίζει. Χρησιμοποιεί το προηγούμενο στοιχείο ListItem για να αποδώσει μια μεμονωμένη εργασία.

import ListItem from "./Item";
function List({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <ListItem key={todo.id} {...todo}></ListItem>
      ))}
    </ul>
  );
}
export default List;

Μπορεί να φαίνεται εντάξει, αλλά τα αντικείμενα todos που ανακτήθηκαν από το REST API λείπουν οι λεπτομερείς πληροφορίες χρήστη. Περιέχουν απλώς το αναγνωριστικό χρήστη.

Για να εμφανιστεί επίσης το όνομα χρήστη, πρέπει να λάβουμε επίσης όλους τους χρήστες και να δημιουργήσουμε ένα νέο λεπτομερές αντικείμενο εργασίας που περιέχει επίσης αυτές τις επιπλέον πληροφορίες.

Η επόμενη συνάρτηση toDetailedTodo παίρνει τη λίστα των χρηστών και επιστρέφει μια συνάρτηση που λαμβάνει μια εκκρεμότητα και επιστρέφει ένα λεπτομερές αντικείμενο που περιέχει το όνομα χρήστη.

function toDetailedTodo(users) {
  return function (todo) {
    return {
      ...todo,
      userName: users
        .find((u) => u.id === todo.userId)?.name
    };
  };
}

Στη συνέχεια, μπορούμε να χρησιμοποιήσουμε την προηγούμενη συνάρτηση toDetailedTodo για να μετατρέψουμε τη λίστα εργασιών σε μια νέα λίστα λεπτομερών εκκρεμών εργασιών και, στη συνέχεια, να εμφανίσουμε τη νέα λίστα στην οθόνη.

import ListItem from "./Item";
function toDetailedTodo(users) {}
function List({ todos, users }) {
  return (
    <ul>
      {todos.map(toDetailedTodo(users)).map((todo) => (
        <ListItem key={todo.id} {...todo}></ListItem>
      ))}
    </ul>
  );
}
export default List;

Αναζήτηση

Το στοιχείο Search εμφανίζει ένα μόνο κείμενο εισαγωγής που λαμβάνει το κείμενο αναζήτησης χρήστη. Έχει μια τοπική κατάσταση που αποθηκεύει αυτό το κείμενο.

Όταν κάνετε κλικ στο κουμπί αναζήτησης, καλείται η επανάκληση onSearch με ένα ερώτημα που περιέχει το κείμενο αναζήτησης.

import { useState } from "react";
function Search({ onSearch }) {
  const [text, setText] = useState("");
  const search = () => {
    const query = { text };
    onSearch?.(query);
  };
  return (
    <form>
      <input 
       onChange={(e) => setText(e.target.value)} 
       value={text} />
      <button onClick={search} type="button">
        Search
      </button>
    </form>
  );
}
export default Search;

App

Το ριζικό στοιχείο App δημιουργεί τη διεπαφή χρήστη χρησιμοποιώντας τα προηγούμενα στοιχεία Search και List. Δημιουργεί επίσης τα δύο καταστήματα, το Todo store και το User store, και στέλνει τα δεδομένα τους στο στοιχείο List. Χειρίζεται το συμβάν onSearch καλώντας τη μέθοδο searchBy στο κατάστημα Todo.

import useTodoStore from "./useTodoStore";
import useUserStore from "./useUserStore";
import List from "./List";
import Search from "./Search";
function App() {
  const [todos, searchBy] = useTodoStore();
  const [users] = useUserStore();
  return (
    <div>
      <h1>Todo App</h1>
      <Search onSearch={searchBy} />
      <List todos={todos} users={users} />
    </div>
  );
}
export default App;

Τελικές σκέψεις

Η πολυεπίπεδη αρχιτεκτονική διαχωρίζει τις ευθύνες της εφαρμογής μεταξύ δικηγόρων. Κάθε ένα από αυτά εξαρτάται από το επίπεδο παρακάτω, αλλά όχι το αντίστροφο.

Τα στοιχεία αντιπροσωπεύουν τμήματα της διεπαφής χρήστη. Αποδίδουν τα δεδομένα κατάστασης στην οθόνη και χειρίζονται την αλληλεπίδραση με τον χρήστη. Αποτελούν μέρος του επιπέδου διεπαφής χρήστη.

Τα καταστήματα διαχειρίζονται την κατάσταση της εφαρμογής. Μπορούν να δημιουργηθούν με προσαρμοσμένους γάντζους. Αποτελούν μέρος του Business Layer.

Τα αντικείμενα βοηθητικού προγράμματος API διαχειρίζονται την επικοινωνία με τα εξωτερικά API. Αποτελούν μέρος του επιπέδου πρόσβασης δεδομένων.

Παρακάτω είναι ένα διάγραμμα με όλα αυτά τα επίπεδα για την εφαρμογή Todo.

Ευχαριστώ για την ανάγνωση.