KMI/WEBA Webové aplikace: Seminář 08

React

Přirozeným krokem po předchozích seminářích je začít tvořit části aplikace nebo dokonce aplikaci celou pomocí JavaScriptu (přesunutí back-endu na frontend). Problémy nám ale bude působit DOM, respektive neefektivita s jakou JS s DOM pracuje (jakákoliv změna na stránce představuje přepočítání a renderování celého DOM).

Tento problém je řešen pomocí virtuálního DOM a shadow DOM. Virtuální DOM představuje vnitřní reprezentaci skutečného DOM. Veškerá práce se odehrává s virtuálním DOM a následně jsou požadované změny (efektivně) promítnuty do skutečného DOM. Shadow DOM umožňuje zapouzdřit část DOM do samostatně stojícího celku (prohlížeč se nemusí starat co je uvnitř).

Další potíže nám způsobuje nízkoúrovňovost JS. I poměrně jednoduché úkony s DOM je nesnadné zapsat. Při zavedení virtuálního DOM se situace ještě zvýrazněně komplikuje. Zásadním konceptem je přechod z imperativního stylu programování na deklarativní, přesněji řečeno na reaktivní programování (varianta deklarativního paradigma).

Virtuální a shadow DOM a stejně tak reaktivní paradigma je adaptováno v mnoha JS framworcích, které jsou určeny pro tvorbu UI komponent nebo webových aplikací. V principu lze na webovou aplikaci nahlížet jako na sadu UI komponent. Příklady takovýchto frameworků jsou React, nebo Vue, Angular, Lit.

Uspokojivě přiblížit všechny hlavní frameworky a jejich filozofii (každý je určený pro nějaký účel) není úplně jednoduché, proto se zaměříme na v současné době jednu z nejrozšířenějších knihoven, knihovnu React.

Zprovoznění

Příště ukážeme, jak vytvářet pomocí Reactu samostatné aplikace, nyní si vystačíme s jednoduchou integraci do webové aplikace/stránky. V principu se takto React běžně používá, má to ale své limitace.

Knihovna má dvě základní komponenty, React a ReactDOM. Níže je ukázáno vložení vývojové verze z CDN.

<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

Pro produkci je nutné použít react.production.min.js a react-dom.production.min.js.

Pro ladění React knihovny existuje řada vývojářských nástrojů, ty ale vyžadují komunikaci skrze HTTP. Je tedy výhodnější (a v některých případech nezbytné) při vývoji použivat webový server.

Nízkoúrovňový přístup

Základem je metoda ReactDOM.createRoot(), která vytváří z vybraného elementu React root DOM element (technicky se zde realizuje propojení s Virtual a shadow DOM). Dodejme, že je ustálené, že React root element má atribut id nastaven na root. Pomocí metody React.createElement() vytváříme elementy, které následně React zobrazuje pomocí metody .render().

// nízkoúrovňový přístup
const root = ReactDOM.createRoot(document.getElementById('root'));
    
// React.createElement(element, properies, children)
const element = React.createElement("h1", {id: "element-1"}, "Hello world");

root.render(element); 

React komponenta

Stěžejním pojmem je React komponenta, které zapouzdřuje část webové aplikace/stránky a vytváří React elementy.

const SG1 = ["Jack O'neill", "Samantha Carter", "Daniel Jackson", "Teal'c"];

// komponenta
function TeamMembers(props) {
  return React.createElement("ul",
  {className: "team-members"},
  props.map((member, i) => 
    React.createElement("li", {key: i}, member)));
}
  
// poznámka: key je pro React, jinak warning
root.render(TeamMembers(SG1));

Komponenty je možné vytvářet i pomocí objektů. Tento způsob vytváření komponent se ale od verze 18 nedoporučuje.

class TeamMembers extends React.Component {
  constructor(props) {
    super(props); // mandatorni
    this.props = props;
  }

  render() {
    return React.createElement("ul",
      {className: "team-members"},
  	  this.props.members.map((member, i) => 
    	  React.createElement("li", {key: i}, member)));
	}
}

root.render(React.createElement(TeamMembers, {members: SG1}, null));

Poznámka: ES6 syntaxi je možné se vyhnout. Dodejme, že mezi uvedenými přístupy je několik rozdílů, jejichž pochopení vyžaduje velmi pokročilé znalosti JS.

Z pohledu programování jsou komponenty pure funkce /čisté funkce), které akceptují vlastnosti (props) a vrací výstup závislý na těchto vlastnostech, přičemž, vlastnosti jsou read-only.

JSX

Nízkoúrovňový přístup názorně ukazuje přístup Reactu k práci s DOM. Pro složitější situace se ale nehodí (je zbytečně komplikovaný). React využívá JSX (JavaScript and XML) zápis pro popis React elementů. Webové prohlížeče této syntaxi nerozumí a proto je třeba překlad do JS (do řeči React.createElement()).

Překlad zajišťuje knihovna Babel. My si ji nyní integrujeme přímo v prohlížeči, ale takové řešení by nemělo být nikdy použito ve finální produkci.

<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

Pro správné fungování musí mít element script atribut type="text/babel".

function TeamMembers(props) {
  return <ul>
    {props.map((member, i) => 
      <li key={i}>{member}</li>)}
      </ul>;
}
  
root.render(TeamMembers(SG1));

Uživatelsky definované komponenty je možné volat přímo v JSX. Mírnou úpravou předešlého získáme čisté řešení z pohledu React.

function TeamMembers(props) {
  return <ul>
    {props.members.map((member, i) => 
      <li key={i}>{member}</li>)}
      </ul>;
}
  
root.render(<TeamMembers members={SG1} />);

JSX zapisuje komponenty jako nepárové elementy (mohou být i párové) a to vždy s /. Jelikož je klíčové slovo class vyhrazeno v JS, zapisuje se atribut class jako className. Výrazy JS se zapisují mezi { }.

React při změně komponenty mění jen nutné části (to vede na obrovské zrychlení oproti práci s klasickým DOM). Například (kód je třeba pohlížet v nástrojích pro vývojáře).

const SG1new = ["Jack O'neill", "Samantha Carter", "Jonas Quinn", "Teal'c"];

setTimeout(() => root.render(<TeamMembers members={SG1new} />), 4000);

Příklad komponenty složené z několika další.

const root = ReactDOM.createRoot(document.getElementById('root'));
function formatDate(date) {
  return date.toLocaleDateString();
}

function Avatar(props) {
	return (
		<img className="avatar"
			src={props.user.avatarUrl}
			alt={props.user.name} />
	);
}

function UserInfo(props) {
	return (
		<div className="user-info">
			<Avatar user={props.user} />
			<div className="user-info__name">
				{props.user.name}
			</div>
		</div>
	);
}

// hlavni komponenta
function Comment(props) {
	return (
		<div className="comment">
			<UserInfo user={props.author} />
			<time className="comment__date">
				{formatDate(props.date)}
			</time>		
			<p className="comment__text">
				{props.text}
			</p>
		</div>
  );
}

// data
const comment = {
	author: {
		name: 'Samantha Carter',
		avatarUrl: 'http://placekitten.com/g/42/42'
	},
	date: new Date(),
	text: 'Chevron Seven Locked!'	
};

// renderování komponenty
root.render(
  <Comment
	author={comment.author}
	date={comment.date}
	text={comment.text} />
);

Pokud chceme aby byl výstup složen z více komponent, je třeba použít fragment, který se v JSX zapisuje mezi <> </>

Stav komponenty

Komponenty si mohou udržovat vlastní stav.

import { useState } from 'react';

function Clock() {
	// vytvoření stavu a nastavení počáteční hodnoty
	const [counter, setCounter] = useState(0);    

	return (
		<div>
			<p>Counter: {counter}</p>
			<button onClick={() => setCounter(counter + 1)}>Increment</button>
		</div>
	);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);

useState je React hook o kterých budeme mluvit příště.

Zpracování událostí

V JSX je mírně jiná syntaxe a chování při zpracování událostí. Výchozí události je třeba bránit explicitně.

function Form() {
  function handleSubmit(e) {
    e.preventDefault();
    console.log('You clicked submit.');
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Submit</button>
    </form>
  );
}

Lze řešit i přes arrow operátor, případně public instance fields (výchozí v Create React App, který ukážeme příště).

Vizualizace komponent

Klasické CSS. V případě inline CSS je třeba využít {{ }} (CSS je JS objekt, vlastnosti nemají pomlčky v názvech) nebo lze CSS vytvářet dynamicky.

<h1 style={{ backgroundColor: 'red' }}>Title</h1>

Zadání

  1. úkol 1

    Vytvořte komponentu Panel, která bude obsahovat tlačítka. Příklad použití a vizualizace následuje. Snažte se o co nejuniverzálnější řešení.

    const buttons = [
      {text: "add", color: "#eee"}, 
      {text: "edit", color: "#eee"},
      {text: "delete", color: "#FF5733", inverse: true}
    ]
          
    const buttons2 = [
      {text: "a", color: "#34eb8f"}, 
      {text: "b", color: "#1f8753", inverse: true},
      {text: "c", color: "#34eb8f"},
      {text: "d", color: "#1f8753", inverse: true},
      {text: "e", color: "#34eb8f"}
    ]
          
    const buttons3 = [
      {text: "info", color: "#349beb", inverse: true}, 
    ]
          
    root.render(
      <>
      <Panel shadow buttons={buttons} />
      <Panel buttons={buttons} />
      <Panel buttons={buttons2} />
      <Panel buttons={buttons3} />
      <Panel shadow buttons={buttons3} />
      </>
    );