Kliensoldali webprogramozás

React alapok. Alap adatkezelés

Horváth Győző
Egyetemi docens
1117 Budapest, Pázmány Péter sétány 1/c., 2.408-as szoba
Tel: (1) 372-2500/8469
horvath.gyozo@inf.elte.hu

Ismétlés

Felhasználói felület kezelése

  • Célok
    • jól skálázódó megoldások
    • jól strukturált, karbantartható kód
    • kényelmes fejlesztés
    • hatékony megoldás
  • → Adat és felület szétválasztása
  • Felület
    • HTML, CSS
    • DOM
    • Eseménykezelők
  • → Egységbe zárás

Felhasználói felület kezelése

  • Kisebb részekre bontás, felelősségi kör szerint
    • → “komponensek”
  • Felület módosításának módja
    • imperatív
    • deklaratív
  • Deklaratív
    • mindig újrarajzoljuk
  • → Dom diffing, virtuális DOM

“Komponens”

import {html, render} from 'https://unpkg.com/lit-html?module';

export class FormView {
  constructor(✒>game, render<✒) {  ✒>// data coming from outside<✒
    this.game = game
    this.renderAll = render
    this.onGenerate = this.onGenerate.bind(this)
  }
  ✒><✒onGenerate(e) { ✒>// event handler<✒
    e.preventDefault()
    const n = document.querySelector('#n').valueAsNumber
    const m = document.querySelector('#m').valueAsNumber
    this.game.initBoard(n, m)
    this.renderAll()
  }
  ✒>render<✒(gameState, n, m) { ✒>// updating the UI in a declarative way<✒
    return html`
      <form action="">
        n = <input type="number" id="n" value="${gameState === 0 ? 3 : n}"> <br>
        m = <input type="number" id="m" value="${gameState === 0 ? 4 : m}">
        <button type="button" @click=${this.onGenerate}>Start new game</button>
      </form>
    `
  }
}

Felhasználói felület kezelése

  • Komponensek
    • egységbe zárás
    • felelősségi kör szerint
  • Deklaratív felületkezelés
    • adat → UI
    • újrarajzolás (igény szerint)
    • DOM diffing, virtuális DOM

React

  • Minden, ami eddig volt, a React óta látjuk ilyen tisztán
  • Az előzmények külön-külön már implementálták
    • UI felosztása (pl. Backbone)
    • DOM diffing (pl. virtual-dom)
    • újrarajzolás (pl. kétirányú adatkötés, Knockout.js)
  • DE
    • hiányosan
    • kényelmetlenül
    • nem hatékonyan
    • nem túl erős koncepcióval

React

  • Egy View library
  • Felületi elemek kezelését könnyíti meg
    • határozott koncepcióval
    • megfelelő támogatással (tooling, doksi)
  • Három elvre épült
    • Komponensek (felelősségi kör szerinti felosztás, egységbe zárás)
    • Egész oldal újrarenderelés (mint PHP-ban)
    • Virtuális DOM (hogy hatékony legyen)
  • Komponensalapú fejlesztés

React

// functional component
function HelloMessage(props) {
  return <div>Hello {props.name}!</div>;
}
ReactDOM.render(<HelloMessage name="Győző" />, mountNode);

Kimenet generálása

  • Felület tervezése
  • Funkcionális egységek megállapítása
  • Komponensekbe szervezése
  • Fastruktúra felépítése
  • Beégetett adat megjelenítése

Példa: lista szűrése

Kompozíció

  • App
    • Card
      • CardHeader
      • FilterForm
      • FilterList
        • FilterListItem
      • CardFooter
export function App() {
  return (
    <div className="container">
      <Card />
    </div>
  )
}
export function Card() {
  return (
    <div className="card text-center">
      <CardHeader />
      <div className="card-body">
        <FilterForm />
        <FilterList />
      </div>
      <CardFooter />
    </div>
  )
}

Adat megjelenítése

Statikus → dinamikus

JSX szabályok

const content = 'Something'
const isAdmin = false
const items = [1, 2, 3]
const color = 'orange'
// content
<div>Some {content}</div>

// elements: always close the tag
<input />

// attributes
<label htmlFor="foo" className="bar" tabIndex="10" />
<div data-attr={content}>Something}</div>

// style attributes
<div style={{backgroundColor: color}}></div>

// conditional
<div>
  { isAdmin
    ? <AdminPanel />
    : <UserPanel /> }
</div>
// or
const panel = isAdmin ? <AdminPanel /> : <UserPanel />
{panel}

// loops
<ul>
  {items.map(e => <li>{e}</li>)}
</ul>

Példák

export function FilterList() {
  const titles = [ 'title1', 'title2', 'title3' ]
  const listItems = titles.map(title => <FilterListItem />)
  return (
    <ul id="code-list" className="list-group">
      {listItems}
    </ul>
  )
}
export function FilterListItem() {
  const title = 'A title'
  return (
    <li className="list-group-item">{title}</li>
  )
}

Példa – űrlap

export function FilterForm() {
  const filterText = 'apple'
  return (
    <form className="form-inline">
      <label htmlFor="inlineFormInputName2">Filter</label>
      <input value={filterText} type="text" className="form-control m-2" id="text-filter" />
      {/* <input defaultValue={filterText} type="text" className="form-control m-2" id="text-filter" /> */}
    </form>
  )
}

Megj.: a value fix, a komponens az adat leképezése, így a beleírt érték sem változhat. Szerkesztéshez a defaultValue-t kell használni

Lokális adatok

  • Megjelennek
  • de nem változtatnak

Properties

Kívülről kapott adatok

Properties

  • Komponens ~ függvény
  • Függvény: x → f(x)
  • Komponens: x → ui(x)
  • Tiszta függvény
    • csak külső adatoktól függ
    • mindig ugyanazt rendeli hozzá
    • jól tesztelhető

Properties

// actual parameter
<Hello name="Győző">
// formal parameter
function Hello(props) {
  return <h1>{props.name}</h1>
}
// formal parameter, object destructuring
function Hello({ name }}) {
  return <h1>{name}</h1>
}

Prop típusok és alapértelmezett értékek

npm install --save prop-types
import PropTypes from 'prop-types';

function Hello({ name = 'Anonymous' }) {
  return <h1>{name}</h1>
}
Hello.propTypes = {
  name: PropTypes.string
}
// ✒>TypeScript<✒
function Hello({ name✒>: string<✒ = 'Anonymous' }) {
  return <h1>{name}</h1>
}

Példa

export function FilterList() {
  const titles = [ 'title1', 'title2', 'title3' ]
  const listItems = titles.map(title => <FilterListItem ✒>title={title}<✒ />)
  return (
    <ul id="code-list" className="list-group">
      {listItems}
    </ul>
  )
}
export function FilterListItem(✒>props<✒) {
  return (
    <li className="list-group-item">✒>{props.title}<✒</li>
  )
}
// or
export function FilterListItem(✒>{title}<✒) {
  return (
    <li className="list-group-item">✒>{title}<✒</li>
  )
}

Adat helye

  • Melyik komponensben tároljuk az adatot?
  • Legközelebbi közös ős vagy feljebb
  • props-ként adjuk le
  • Data Down
  • App
    • Card (lista, szűrő érték)
      • CardHeader
      • FilterForm (szűrő érték)
      • FilterList (lista → szűrt lista)
        • FilterListItem (cím)
      • CardFooter

Példa

export function Card() {
  const titles = [ 'title1', 'title2', 'item3' ]
  const filterText = 'title'

  return (
    <div className="card text-center">
      <CardHeader />
      <div className="card-body">
        <FilterForm filterText={filterText} />
        <FilterList filterText={filterText} list={titles} />
      </div>
      <CardFooter />
    </div>
  )
}
export function FilterList({list, filterText}) {
  const titles = list.filter(e => e.includes(filterText))
  const listItems = titles.map(title => <FilterListItem title={title} />)
  return (
    <ul id="code-list" className="list-group">
      {listItems}
    </ul>
  )
}

State

Belső állapot

Újrarenderelés

  • Újrarenderelés kiváltó okai
    • React.render() direkt hívás
    • Prop megváltozik
    • Belső állapotváltozás
  • Belső állapotváltozás oka
    • esemény
    • időzítő
    • külső hatás

Belső állapot

  • Belső állapot – példák
    • pl. egy input mező értéke
    • pl. kinyitható elem nyitott/zárt állapota
  • Belső állapot
    • időben változó adat
    • az alkalmazáshoz tartozó adat
  • Nem állapot
    • property
    • számolt érték
    • időben állandó érték

Belső állapot

useState

import React, { useState } from "react";

const randomColor = () =>`hsl(${random(0, 360)}, 50%, 50%)`

function ColorBox() {
  ✒>const [color, setColor] = useState(randomColor())<✒
  return (
    <div style={{ backgroundColor: ✒>color<✒ }} onClick={() => ✒>setColor(randomColor())<✒}>
      React rendered box, random value: {random(1, 100)}
    </div>
  )
}

Példa

Alkalmazásszintű adat

function Card() {
  const [filterText, setFilterText] = useState('');
  const [titles, setTitles] = useState([ 'title1', 'title2', 'item3' ]);
  
  return (
    <div className="card text-center">
      <CardHeader />
      <div className="card-body">
        <FilterForm filterText={filterText} />
        <FilterList filterText={filterText} list={titles} />
      </div>
      <CardFooter />
    </div>
  )
}

Eseménykezelés

Eseménykezelés

  • ~ inline eseménykezelő
  • függvényreferencia hozzárendelése
  • automatikus delegálás
  • szintetikus eseményobjektum
  • camelCase eseményattribútum
function MyComponent() {
  function handleClick(e) {
    console.log('Button was clicked')
  }
  return (
    <button ✒>onClick={handleClick}<✒>Click</button>
  )
}

Példa

const random = (a, b) => Math.floor(Math.random() * (b - a + 1)) + a;
const randomColor = () =>`hsl(${random(0, 360)}, 50%, 50%)`

function ColorBox(props) {
  const [color, setColor] = useState(randomColor())
  const changeColor = () => {
    setColor(randomColor())
  }
  render() {
    return (
      <div style={{ backgroundColor: color }} onClick={changeColor}>
        React rendered box, random value: {random(1, 100)}
      </div>
    )
  }
}

Példa

Controlled form component

function FilterForm({ filterText: initialFilterText })  {
  const [filterText, setFilterText] = useState(initialFilterText)

  const handleInput = e => {
    setFilterText(e.target.value)
  }

  return (
    <form className="form-inline">
      <label htmlFor="inlineFormInputName2">Filter</label>
      <input value={filterText} onChange={this.handleInput} type="text" className="form-control m-2" />
    </form>
  )
}

Példa

  • Data down, “action up”
  • Data down, action down
// Card.js
function Card() {
  // Component state
  const [filterText, setFilterText] = useState('');
  const [titles, setTitles] = useState([ 'title1', 'title2', 'item3' ]);
  // Computed values
  const filteredTitles = titles.filter(e => e.includes(filterText))
  // Event handlers
  const changeFilterText = newVal => setFilterText(newVal)

  return (
    <div className="card text-center">
      <CardHeader />
      <div className="card-body">
        <FilterForm filterText={filterText} ✒>onFilterTextChange={changeFilterText}<✒ />
        <FilterList list={filteredTitles} />
      </div>
      <CardFooter />
    </div>
  )
}
// FilterForm.js
function FilterForm({filterText, ✒>onFilterTextChange<✒}) {
  return (
    <form className="form-inline">
      <label htmlFor="inlineFormInputName2">Filter</label>
      <input value={filterText} ✒>onChange={e => onFilterTextChange(e.target.value)}<✒ type="text" className="form-control m-2" />
    </form>
  )
}

További tudnivalók

Listák: key

  • Listák (tömbök) esetén a hatékony változáskövetéshez azonosítani szükséges a listaelemeket
  • key attribútum
  • legyen egyedi!
  • pl. id
export function FilterList({list: titles}) {
  const listItems = titles.map(title => <FilterListItem ✒>key={title}<✒ title={title} />)
  return (
    <ul id="code-list" className="list-group">
      {listItems}
    </ul>
  )
}

Imperatív módosítások

  • React alapvetően deklaratív
    • módosításhoz újrarendereljük
  • Néha imperatív módosításra van szükség
    • Fókusz, szövegkijelölés, médialejátszás
    • Animációk triggerelése
    • Más DOM könyvtárakkal való együttműködés
  • → refs

Ref

useRef

function TextInputWithFocusButton() {
  ✒>const inputEl = useRef(null);<✒
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    ✒>inputEl.current<✒.focus();
  };
  return (
    <>
      <input ✒>ref={inputEl}<✒ type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

Ref – példa

function FilterForm({ filterText, onFilterTextChange }) {
  const inputEl = useRef(null);

  useEffect(() => {
    inputEl.current.focus()
  }, [])

  return (
    <form className="form-inline">
      <label htmlFor="inlineFormInputName2">Filter</label>
      <input
        value={filterText}
        onInput={(e) => onFilterTextChange(e.target.value)}
        ref={inputEl}
        type="text" className="form-control m-2" id="text-filter"
      />
    </form>
  );
}

Uncontrolled form components

  • Űrlapadatot nem a React, hanem a DOM kezeli
  • defaultValue
function NameForm() {
  const inputEl = useRef(null);
  
  const handleSubmit = (event) => {
    alert('A name was submitted: ' + inputEl.current.value);
    event.preventDefault();
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input type="text" ref={inputEl} defaultValue="default-text" />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}

Stílusok kezelése

  • import 'bootstrap/dist/css/bootstrap.css'
    • globál import
  • className="active"
  • style={{ top: '10px' }}
  • classnames csomag
  • import "./my-component.module.css";
  • component library

classnames csomag

npm install classnames --save
classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'
import classnames from "classnames";

export function MyButton({ text, isBasic = true, color = "red", icon = "plus" }) {
  const styles = {
    button: classnames("ui", "button", {
      // referencing a CSS class by name
      basic: isBasic,
      // dynamically referencing a CSS class
      [color]: true
    }),
    icon: classnames("icon", icon)
  };

  return (
    <button className={styles.button}> 
      <i className={styles.icon}></i>
      {text}
    </button>
  );
}

CSS modules

Lokális hatókörű stílusosztályok

/* Button.module.css */
.error {
  background-color: red;
}
/* another-stylesheet.css */
.error {
  color: red;
}
// Button.js
import React from 'react';
import styles from './Button.module.css'; // Import css modules stylesheet as styles
import './another-stylesheet.css'; // Import regular stylesheet

class Button extends Component {
  // reference as a js object
  return <button className={styles.error}>Error Button</button>;
}

Routing

Routing

  • Logika rendelése URL végponthoz
  • Oldalnavigáció
  • Címsor kezelése
  • React router
npm install react-router-dom

Alap routing és URL paraméterek

import { render } from "react-dom";
import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom";

render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);

function App() {
  return (
    <Routes>
      <Route path="invoices" element={<Invoices />}>
        <Route path=":invoiceId" element={<Invoice />} />
        <Route path="sent" element={<SentInvoices />} />
      </Route>
    </Routes>
  );
}

function Invoices() {
  return (
    <div>
      <h1>Invoices</h1>
      <Outlet />
    </div>
  );
}

function Invoice() {
  let { invoiceId } = useParams();
  return <h1>Invoice {invoiceId}</h1>;
}

function SentInvoices() {
  return <h1>Sent Invoices</h1>;
}

Kompozíció

Komponensek egymásba ágyazása

Öröklés helyett

Tartalmazás

Ha ismert előre a gyerekkomponens:

const App = () => <Parent />

const Parent = () => 
  <div>Parent 
    <Child /> 
  </div>

const Child = () => <div>Child</div>

Tartalmazás

  • Ha nem ismert előre a gyerekkomponens
  • Container
  • ~ a legtöbb HTML elem
  • props.children
const App = () =>
  <Parent>
    <Child />  
  </Parent>

const Parent = ({children}) => 
  <div>Parent 
    {children} 
  </div>

const Child = () => <div>Child</div>
const App = () =>
  <Parent children={<Child />} />

const Parent = ({children}) => 
  <div>Parent 
    {children} 
  </div>

const Child = () => <div>Child</div>

Tartalmazás

  • Több slot
  • Kívülről, paraméterként megkapott komponensek
const App = () =>
  <Parent
    slot1={<Child1 />}
    slot2={<Child2 />}
  />

const Parent = ({slot1, slot2}) => 
  <div>Parent 
    <div>{slot1}</div>
    <div>{slot2}</div>
  </div>

const Child1 = () => <div>Child1</div>
const Child2 = () => <div>Child2</div>

Specializáció

  • egyik komponens a másik speciális esete
  • a specifikus komponens rendereli a felparaméterezett általánost
const App = () => <Specific />

const Specific = () => 
  <General name="Welcome" description="Hello message" />

const General = ({name, description}) => 
  <div>
    <div>{name}</div>
    <div>{description}</div>
  </div>

Függvény prop-ok

  • Olyan prop-ok, amiknek értéke függvény
  • Külső komponens függvényként saját logikáját injektálja a gyerekkomponensbe
  • Gyerekkomponens ezt meghívhatja
  • Saját belső adatát átadva a külső komponensnek
  • Action up = Data up
  • Callback minta
  • Inversion of Control minta

Függvény prop-ok

const Card = () => {
  const [filterText, setFilterText] = useState('title')

  ✒>const changeFilterText = newVal => setFilterText(newVal)<✒
  
  return <FilterForm 
    ✒>onFilterTextChange={this.changeFilterText}<✒ 
    filterText={this.state.filterText} />
}
function FilterForm({filterText, ✒>onFilterTextChange<✒}) {
  return <input 
    value={filterText} 
    onInput={e => ✒>onFilterTextChange(e.target.value)<✒} 
    type="text" />
}

Render prop

  • Speciális függvény prop: render
  • A függvény React elemet ad vissza
  • Külső komponens ezzel a függvénnyel hívja meg a gyerekkomponenst
  • Gyerekkomponens saját render logikája helyett a függvényt hívja meg
<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>
const DataProvider = ({render}) => {
  const innerData = 42
  return render(innerData)
}

Render prop

  • Kívülről határozhatjuk meg egy gyerekkomponens tartalmát (~tartalmazás)
  • Gyerekkomponens saját belső adatával hívhatja meg
  • Állapot, viselkedés megosztása komponensek között
<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

Render prop

Mouse and Cat (React dokumentáció)

Render prop

  • Render prop neve bármi lehet
  • pl. children
<Mouse children={mouse => (
  <p>The mouse position is {mouse.x}, {mouse.y}</p>
)}/>
<Mouse>
  {mouse => (
    <p>The mouse position is {mouse.x}, {mouse.y}</p>
  )}
</Mouse>
Mouse.propTypes = {
  children: PropTypes.func.isRequired
};

const App = () =>
  <Parent>
    <Child />  
  </Parent>

const Parent = ({children}) => 
  <div>Parent 
    {children} 
  </div>



const Child = () => <div>Child</div>
const App = () =>
  <Parent>
    {data => <Child text={data} />}
  </Parent>

const Parent = ({children}) => {
  const [data] = useState(42)
  return <div>Parent 
    {children(data)} 
  </div>
}

const Child = ({text}) => <div>{text}</div>

Osztálykomponensek

Osztálykomponens

class Hello extends React.Component {
  render() {
    return <h1>Hello world!</h1>
  }
}

Properties

// formal parameter, class component
class Hello extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    return <h1>{props.name}</h1>
  }
}

Belső állapot

this.state, this.setState

const randomColor = () =>`hsl(${random(0, 360)}, 50%, 50%)`

class ColorBox extends React.Component {
  constructor(props) {
    super(props)
    ✒>this.state = { color: randomColor() }<✒
  }
  changeColor = () => {
    ✒>this.setState({ color: randomColor() })<✒
  }
  render() {
    return (
      <div style={{ backgroundColor: ✒>this.state.color<✒ }}>
        React rendered box, random value: {random(1, 100)}
      </div>
    )
  }
}

Eseménykezelés

const random = (a, b) => Math.floor(Math.random() * (b - a + 1)) + a;
const randomColor = () =>`hsl(${random(0, 360)}, 50%, 50%)`

class ColorBox extends React.Component {
  constructor(props) {
    super(props)
    this.state = { color: randomColor() }
    this.changeColor = this.changeColor.bind(this)
  }
  changeColor() {
    this.setState({ color: randomColor() })
  }
  render() {
    return (
      <div style={{ backgroundColor: this.state.color }} onClick={this.changeColor}>
        React rendered box, random value: {random(1, 100)}
      </div>
    )
  }
}

Eseménykezelők hozzárendelése

this kontextusa!

class ColorBox extends React.Component {
  changeColor() { /* ... */ }
  render() {
    return (
      <div  onClick={e => this.changeColor(e)} />
    )
  }
}

class ColorBox extends React.Component {
  changeColor() { /* ... */ }
  render() {
    return (
      <div  onClick={this.changeColor.bind(this)} />
    )
  }
}

class ColorBox extends React.Component {
  constructor(props) {
    this.changeColor = this.changeColor.bind(this)
  }
  changeColor() { /* ... */ }
  render() {
    return (
      <div  onClick={this.changeColor} />
    )
  }
}

class ColorBox extends React.Component {
  changeColor = () => { /* ... */ }
  render() {
    return (
      <div  onClick={this.changeColor} />
    )
  }
}

Életciklus események

class Hello extends React.Component {
  constructor(props) {}
  componentDidMount() {}
  componentWillUnmount() {}
  shouldComponentUpdate(nextProps, nextState) {}
  componentDidUpdate(prevProps, prevState, snapshot)
}

Életciklus példa

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }
  componentDidMount() {
    this.timerID = setInterval(() => this.tick(), 1000);
  }
  componentWillUnmount() {
    clearInterval(this.timerID);
  }
  tick = () => { this.setState({ date: new Date() }); }
  render() {
    return (
      <div>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Ref

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    ✒>this.myRef = React.createRef();<✒
  }
  someWhere() {
    const node = ✒>this.myRef.current;<✒
  }
  render() {
    return <div ✒>ref={this.myRef}<✒ />;
  }
}

Ref – példa

export class FilterForm extends React.Component {
  constructor(props) {
    super(props)
    this.input = React.createRef()
  }
  componentDidMount() {
    this.input.current.focus()
  }
  render() {
    const {filterText, onFilterTextChange} = this.props
    return (
      <form className="form-inline">
        <label htmlFor="inlineFormInputName2">Filter</label>
        <input ref={this.input} value={filterText} onChange={e => onFilterTextChange(e.target.value)} type="text" className="form-control m-2" id="text-filter" />
        {/* <input defaultValue={filterText} type="text" className="form-control m-2" id="text-filter" /> */}
      </form>
    )
  }
}

Végszó

  • React alapok
    • kiírás
      • statikus → komponensek
      • props
      • state
    • bemenet
      • események
      • setState
    • Data down, action down
  • Osztálykomponensek
  • Stílusok
  • Routing