Kliensoldali webprogramozás

Adat helye. Adattárolá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

React

  • Komponensek
    • felhasználói felület funkcionális egységekre bontása
  • Újrarajzolás változás esetén
    • deklaratív megközelítés
    • ui = f(state)
    • funkcionális megközelítések
  • Hatékony kirajzolás
    • Virtual DOM
    • DOM diffing

React alapok

  • kiírás
    • statikus → komponensek
  • adat
    • props: kívülről érkező, immutábilis
    • state: belső, mutábilis
  • bemenet
    • események
    • setState
  • Data down, action down
  • Életciklus függvények

Adat helye

Data down, action up

Adatkezelési segédletek

  • Kompozíció
  • Prop drilling
  • Context
  • Hook-ok
    • React szolgáltatásainak elérése a függvénykomponensekben
    • Fejlettebb programozási minták

Adat helye

Állapottér

  • Az alkalmazás az adatról szól
  • Adat és nézet szétválasztása
  • Az alkalmazás leírásához és működtetéséhez szükséges adatok összességét állapottérnek hívjuk
  • Kérdések
    • state vagy prop? → state
    • hol legyen definiálva?
    • hogyan legyen strukturálva?

Állapotkezelés

  • Állapot: az adatok összessége
  • Állapotkezelő függvények: állapot-átmenetet idéznek elő
let counter = 0
function increaseCounter(step = 1) {
  counter += step
}
function decreaseCounter(step = 1) {
  counter -= step
}
class Counter {
  constructor() {
    this.counter = 0
  }
  increaseCounter(step = 1) {
    this.counter += step
  }
  decreaseCounter(step = 1) {
    this.counter -= step
  }
}

React state

function Counter() {
  const [counter, setCounter] = useState(0)
  function increaseCounter(step = 1) {
    setCounter(counter => counter + step)
  }
  function decreaseCounter(step = 1) {
    setCounter(counter => counter - step)
  }
  return <div>
    <button onClick={() => decreaseCounter()}>-</button>
    {counter}
    <button onClick={() => increaseCounter()}>+</button>
  </div>
}

ReactDOM.render(
  <Counter />,
  document.getElementById('root')
);

Egységbe zárás

  • Saját hook készítése
  • Logika (és nem adat) újrahasznosítása
function useCounter(initVal) {
  const [counter, setCounter] = useState(initVal)
  function increaseCounter(step = 1) {
    setCounter(counter => counter + step)
  }
  function decreaseCounter(step = 1) {
    setCounter(counter => counter - step)
  }
  return { counter, increaseCounter, decreaseCounter }
}

function Counter() {
  const {counter, increaseCounter, decreaseCounter} = useCounter(0)
  return <div>
    <button onClick={() => decreaseCounter()}>-</button>
    {counter}
    <button onClick={() => increaseCounter()}>+</button>
  </div>
}

function App() {
  return <>
    <Counter />
    <Counter />
  </>
}

Közös adat

Context

const CounterContext = React.createContext()
function CounterProvider({children}) {
  const counterInstance = useCounter(0)
  return <CounterContext.Provider value={counterInstance} children={children} />
}

function Counter() {
  const {counter, increaseCounter, decreaseCounter} = useContext(CounterContext)
  return <div>
    <button onClick={() => decreaseCounter()}>-</button>
    {counter}
    <button onClick={() => increaseCounter()}>+</button>
  </div>
}

function App() {
  return (
    <CounterProvider>
      <Counter />
      <Counter />
    </CounterProvider>
  )
}

Még egy adatcsoport

function useStepper(initVal) {
  const [step, setStep] = useState(initVal)
  return { step, setStep }
}

const StepContext = React.createContext()
function StepProvider({children}) {
  const stepInstance = useStepper(1)
  return <StepContext.Provider value={stepInstance} children={children} />
}

function Counter() {
  const {step, setStep} = useContext(StepContext)
  const {counter, increaseCounter, decreaseCounter} = useContext(CounterContext)
  return <div>
    <button onClick={() => increaseCounter(step)}>+</button>
    {counter}
    <button onClick={() => decreaseCounter(step)}>-</button>
  </div>
}

function App() {
  return (
    <StepProvider>
      <CounterProvider>
        <Counter />
        <Counter />
      </CounterProvider>
    </StepProvider>
  )
}

Provider-ek sorrendje?

Kölcsönös függés

  • Counter használja a step-et
  • Step értéke a countertől függ
  • Logika a nézetben
function Counter() {
  const {step, setStep} = useContext(StepContext)
  const {counter, increaseCounter, decreaseCounter} = useContext(CounterContext)

  const increase = () => {
    const exp = Math.floor(Math.log10(counter))
    const s = 10**exp
    setStep(s)
    increaseCounter(s)
  }
  const decrease = () => {
    const exp = Math.ceil(Math.log10(counter)) - 1
    const s = 10**exp
    setStep(s)
    decreaseCounter(s)
  }

  return <div>
    <button onClick={decrease}>-</button>
    {counter}
    <button onClick={increase}>+</button>
  </div>
}

Közös réteg

const ActionContext = React.createContext()
function ActionProvider({children}) {
  const {step, setStep} = useContext(StepContext)
  const {counter, increaseCounter, decreaseCounter} = useContext(CounterContext)
  
  const increase = () => {
    const exp = Math.floor(Math.log10(counter))
    const s = 10**exp
    setStep(s)
    increaseCounter(s)
  }
  const decrease = () => {
    const exp = Math.ceil(Math.log10(counter)) - 1
    const s = 10**exp
    setStep(s)
    decreaseCounter(s)
  }

  const actions = {increase, decrease}

  return <ActionContext.Provider value={actions} children={children} />
}

function Counter() {
  const {increase, decrease} = useContext(ActionContext)
  const {counter} = useContext(CounterContext)

  return <div>
    <button onClick={decrease}>-</button>
    {counter}
    <button onClick={increase}>+</button>
  </div>
}

Hatékonyítás

const ActionContext = React.createContext()
function ActionProvider({children}) {
  const {step, setStep} = useContext(StepContext)
  const {counter, increaseCounter, decreaseCounter} = useContext(CounterContext)
  
  const increase = useCallback(() => {
    const exp = Math.floor(Math.log10(counter))
    const s = 10**exp
    setStep(s)
    increaseCounter(s)
  }, [counter])
  const decrease = useCallback(() => {
    const exp = Math.ceil(Math.log10(counter)) - 1
    const s = 10**exp
    setStep(s)
    decreaseCounter(s)
  }, [counter])

  const actions = useMemo(() => ({ increase, decrease}), [counter])

  return <ActionContext.Provider value={actions} children={children} />
}

Most egyelőre nem fontos; useReducer használatával még elegánsabb

Összegezve

  • Állapotkezelés React-ben: useState
  • Adat (useState) és metódus egységbezárása: saját hook
  • Mindenki számára elérhető: App szinten + prop drilling
  • Mindenki számára elérhető: Contextben
  • Adat strukturálása
    • külön context-ekben
    • egymás használata (useContext), de a sorrend fontos
    • közös service-ek definiálása
  • Limitációk

Adattárolás

Függetlenül az adat helyétől

Kitekintés

Tárolási módok

  • Böngészőben
    • Web Storage API (szinkron)
      • localStorage
      • sessionStorage
    • indexedDB (aszinkron)
    • Függvénykönyvtár magas szintű függvényekkel
  • Szerveren (felhőben)
    • REST API
    • GraphQL
    • egyéb üzenetküldő protokoll

nedb

  • Github
  • Dokumentum alapú adatbázis
    • nincs séma
    • JSON objektumokat tárolunk el
    • pl. {title: 'Track1', color: 'red', filters: [1, 3]}
  • multi-environment
    • node.js
    • electron
    • böngésző
npm install --save nedb

nedb

import Nedb from "nedb/browser-version/out/nedb.min";

const db = new Nedb({ filename: 'data.nedb', autoload: true });   

//Inserting document(s)
db.insert(doc, function (err, newDoc) {   // Callback is optional
  // newDoc is the newly inserted document, including its _id
  // newDoc has no key called notToBeSaved since its value was undefined
});

//Finding document(s)
db.find({ foo: 'bar' }, function (err, docs) {
  // docs is an array containing documents Mars, Earth, Jupiter
  // If no document is found, docs is equal to []
});

db.findOne({ _id: 'id1' }, function (err, doc) {
  // doc is the document Mars
  // If no document is found, doc is null
});

//Replacing a document
db.update({ planet: 'Jupiter' }, { planet: 'Pluton'}, {}, function (err, numReplaced) {
  // numReplaced = 1
  // The doc #3 has been replaced by { _id: 'id3', planet: 'Pluton' }
  // Note that the _id is kept unchanged, and the document has been replaced
});

//Deleting a document
db.remove({ _id: 'id2' }, {}, function (err, numRemoved) {
  // numRemoved = 1
});

Használat

import Nedb from "nedb/browser-version/out/nedb.min";
export const planetsDb = new Nedb({ filename: 'data.nedb', autoload: true });   
import { planetsDb } from "./data/planets";

function App() {
  const [planets, setPlanets] = useState([])
  useEffect(() => {
    planetsDb.find({}, function (err, docs) {
      setPlanets(docs)
    })
  }, [])
  return (
    <ul>
      {planets.map(planet => <li key={planet._id}>{planet.planet}</li>)}
    </ul>
  );
}

Köztes réteg

  • Adattárolásért felelős réteg
    • in-memory
    • böngészőben
    • szerveren
  • Absztrakt műveletekkel
  • Aszinkron interfésszel (Promise)
class TrackStorage {
  constructor()           { /* ... */ }
  async find(filter = {}) { /* ... */ }
  async insert(track)     { /* ... */ }
  async update(id, track) { /* ... */ }
  async remove(id)        { /* ... */ }
}
const trackStorage = new TrackStorage()
const tracks = await trackStorage.find()
await trackStorage.insert({title: 'Track1', color: 'red', filters: [1, 3]})

Callback → Promise

const findTracks = (filter = {}) => {
  return new Promise((resolve, reject) => {
    tracksDb.find(filter, function (err, docs) {
      if (err) { reject(err)   } 
      else     { resolve(docs) }
    })
  })
}
const promisify = fn => (...args) => {
  return new Promise((resolve, reject) => {
    fn(...args, (err, data) => {
      if (err) {
        reject(err)
      } else {
        resolve(data);
      }
    });
  });
}
// For example
const findTracks = promisify(tracksDb.find)

TrackStorage

class TrackStorage {
  constructor(tracksDb) {
    this.tracksDb = {}
    const methods = ['find', 'findOne', 'insert', 'update', 'remove']
    methods.forEach(method => this.tracksDb[method] = promisify(tracksDb[method].bind(tracksDb)))
  }
  async find(filter = {}) {
    return await this.tracksDb.find(filter)
  }
  async insert(track) {
    return await this.tracksDb.insert(track)
  }
  async update(id, track) {
    return await this.tracksDb.update({_id: id}, track, { returnUpdatedDocs: true })
  }
  async remove(id) {
    return await this.tracksDb.remove({_id: id})
  }
}
const [tracks, setTracks] = useState([])
const addNewTrack = async track => {
  const newTrack = await tracksStorage.create(track)
  setTracks([...tracks, newTrack])
}

Optimalizáció

Rerender

  • Előre ne optimalizálj!
  • A React gyors!
  • Alapból a React mindegyik elemet újrarendereli, majd eldönti, melyikhez kell DOM módosítás
  • Osztálykomponensek
    • React.Component
    • React.PureComponent
  • Függvénykomponensek
    • React.memo
    • useMemo
    • useCallback

shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState) {
  return true;
}

shouldComponentUpdate

class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

PureComponent

Sekély összehasonlítás

class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

React.memo

  • HOC
  • Függvénykomponensekhez
  • Sekély összehasonlítás
  • Csak props
const MyComponent = React.memo(function MyComponent(props) {
  /* render using props */
});

Optimalizáció

memo, PureComponent

import React, { memo } from 'react';

// 🙅‍♀️ if parent renders, it renders too
const ComponentB = (props) => {
  return <div>{props.propB}</div>
};

// 🙆‍♂️ changes when props changes
const ComponentB = memo((props) => {
  return <div>{props.propB}</div>
});

Eseménykezelők

class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }
  render() {
    return (
      <button onClick={() => this.handleClick()}>
        Click me
      </button>
    );
  }
}
class LoggingButton extends React.Component {
  handleClick = () => {
    console.log('this is:', this);
  }
  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

Optimalizáció

Névtelen függvények

function Foo() {
  return (
    <button onClick={() => console.log('boop')}> // 😕
      BOOP
    </button>
  );
}
function Foo() {
  const handleClick = () => console.log('boop') // 😕
  return (
    <button onClick={handleClick}> 
      BOOP
    </button>
  );
}

Optimalizáció

Névtelen függvények

const handleClick = () => console.log('boop');
function Foo() {
  return (
    <button onClick={handleClick}> // 😀
      BOOP
    </button>
  );
}
function Foo() {
  const handleClick = useCallback(() => console.log('boop'), []);
  return (
    <button onClick={handleClick}> // 😀
      BOOP
    </button>
  );
}

Optimalizáció

Objektumliterál, memo sem segít

const ComponentA = () => (
  <div>
    <ComponentB style={{ color: 'blue', background: 'gold' }}/> // 😕
  </div>
)
const ComponentB = (props) => (
  <div style={this.props.style}>Something</div>
)
const myStyle = { color: 'blue', background: 'gold' } // 😀
const ComponentA = () => (
  <div>
    <ComponentB style={myStyle}/>
  </div>
)
const ComponentB = (props) => (
  <div style={this.props.style}>Something</div>
)

Higher-order components

Higher-order functions

  • Matematikailag olyan függvény, amely
    • vagy függvényt kap paraméterül
    • vagy függvényt ad vissza
const twice = (f, v) => f(f(v));
const add3 = v => v + 3;
twice(add3, 7); // 13
// Függvénygenerátor
function greaterThan(n) {
  return m => m > n;
}
let greaterThan10 = greaterThan(10);
console.log(greaterThan10(11)); // true
function unless(test, then) {
  if (!test) then();
}
unless(0 % 2 === 1, () => {
  console.log(n, "is even");
});

Higher-Order Component

  • Technika komponenslogika újrahasznosítására
  • React kompozíciós természetéből jön
  • HOC: egy függvény, ami paraméterül kap egy komponenst és visszaad egy új komponenst
  • react-redux: connect
  • Hook-ok mellett nincs nagy szükség rájuk
const EnhancedComponent = higherOrderComponent(WrappedComponent);

Példa

class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id)
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id)
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}
const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
  // ...and returns another component...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ... that takes care of the subscription...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... and renders the wrapped component with the fresh data!
      // Notice that we pass through any additional props
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

Végszó

  • Állapotkezelés
  • Optimalizáció
  • High-Order Components