Poznajemy jeszcze kilka zagadnień powiązanych z hookiem useEffect, które wcześniej mogliśmy przeoczyć. Do dzieła.

W poprzedniej lekcji poznaliśmy sobie taką animację, a nawet napisaliśmy jej przeciwieństwo:

export class FadeInAnimation {
  constructor(node) {
    this.node = node;
  }
  start(duration) {
    this.duration = duration;
    if (this.duration === 0) {
      // Jump to end immediately
      this.onProgress(1);
    } else {
      this.onProgress(0);
      // Start animating
      this.startTime = performance.now();
      this.frameId = requestAnimationFrame(() => this.onFrame());
    }
  }
  onFrame() {
    const timePassed = performance.now() - this.startTime;
    const progress = Math.min(timePassed / this.duration, 1);
    this.onProgress(progress);
    if (progress < 1) {
      // We still have more frames to paint
      this.frameId = requestAnimationFrame(() => this.onFrame());
    }
  }
  onProgress(progress) {
    this.node.style.opacity = progress;
  }
  stop() {
    cancelAnimationFrame(this.frameId);
    this.startTime = null;
    this.frameId = null;
    this.duration = 0;
  }
}

Teraz użyjemy sobie tej animacji w React. Najpierw App:

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

Co tu mamy:

  • stan, doyślnie false
  • na onClick zamiana stanu na jego przeciwieństwo
  • conditional rendering – ternary operator (remove albo show)
  • conditional rendering – logic AND, jeżeli show prawdziwe, wyrenderuj mi komponent Welcome

Ok, a sam komponent Welcome? Wygląda tak:

function Welcome() {
  const ref = useRef(null);

  useEffect(() => {
    const animation = new FadeInAnimation(ref.current);
    animation.start(1000);
    return () => {
      animation.stop();
    };
  }, []);

  return (
    <h1
      ref={ref}
      style={{
        opacity: 0,
        color: 'white',
        padding: 50,
        textAlign: 'center',
        fontSize: 50,
        backgroundImage: 'radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%)'
      }}
    >
      Welcome
    </h1>
  );
}

Mamy ref, który pokazuje h1, mamy style tego h1 w reactowej konwencji, mamy useEffect, który ma ruszyć na pierwszy render, on zaś wykorzystuje FadeInAnimation, do konstruktora przekazując ref.current jako node.

Ok, drugi przykład, będą tam dzieci i sloty w React, które w tej bibliotece są porpsami. App:

import { useState } from 'react';
import ModalDialog from './ModalDialog.js';

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(true)}>
        Open dialog
      </button>
      <ModalDialog isOpen={show}>
        Hello there!
        <br />
        <button onClick={() => {
          setShow(false);
        }}>Close</button>
      </ModalDialog>
    </>
  );
}

Widzimy, że:

  • mamy stan, domyślnie fałsz
  • mamy button, onClick ustawia stan na true
  • mamy komponent ModalDialog, którego prop isOpen jest stanem show rodzica (App)
  • komponent ModalDialog ma otwarcie i zamknięcie, zatem używamy slotu, zawartość tego slotu jest pod props.children
  • z pozycji tego slotu (props.children) mamy button wykorzystujący setShow rodzica

Powiedzmy to raz jeszcze – nie zmieniamy propu isOpen, zmieniamy stan rodzica, który powoduje re-render, także komponentu dziecka, ze zmienionym już propem isOpen.

Ok, a sam modal jak wygląda? Tak:

import { useEffect, useRef } from 'react';

export default function ModalDialog({ isOpen, children }) {
  const ref = useRef();

  useEffect(() => {
    if (!isOpen) {
      return;
    }
    const dialog = ref.current;
    dialog.showModal();
    return () => {
      dialog.close();
    };
  }, [isOpen]);

  return <dialog ref={ref}>{children}</dialog>;
}

Jak widać:

  • Element dialog (zwykły HTML5) ma ref
  • Mamy dwa propsy – isOpen, oraz zawartość slotu (markup między otwierającym a zamykajacym tagiem ModalDialog) jako children
  • Jeżeli isOpen nie jest prawdziwe, uciekamy
  • Jeżeli jest, pokazujemy modal
  • Zamknięcie modalu jako cleanup
  • isOpen to zależność, ponieważ korzystamy z tego propsa

Dodam, że metody dialog.showModal i close to czysty JS czystego HTML5:

const dialog = document.querySelector("dialog");
const showButton = document.querySelector("dialog + button");
const closeButton = document.querySelector("dialog button");

// "Show the dialog" button opens the dialog modally
showButton.addEventListener("click", () => {
  dialog.showModal();
});

// "Close" button closes the dialog
closeButton.addEventListener("click", () => {
  dialog.close();
});

Bez tych metod nie będzie backdropu. Raz jeszcze – dialog HTML5 bez backdropu, otwarty atrybutem open:

<dialog open>
  <p>Greetings, one and all!</p>
  <form method="dialog">
    <button>OK</button>
  </form>
</dialog>

Jak chcemy backdrop, połączymy HTML5, CSS i JS:

<dialog>
  <button autofocus>Close</button>
  <p>This modal dialog has a groovy backdrop!</p>
</dialog>
<button>Show the dialog</button>

Dialog zamknięty, close button ma autofocus, też fajny atrybut. Drugi button jest widoczny od razu.

Łapiemy te elementy w JS:

const dialog = document.querySelector("dialog");
const showButton = document.querySelector("dialog + button");
const closeButton = document.querySelector("dialog button");

W CSS stylujemy backdrop (to tło):

::backdrop {
  background-image: linear-gradient(
    45deg,
    magenta,
    rebeccapurple,
    dodgerblue,
    green
  );
  opacity: 0.75;
}

JavaScript wykorzystuje metody klasy HTMLDialogElement aby pokazywać i ukrywać dialog:

// "Show the dialog" button opens the dialog modally
showButton.addEventListener("click", () => {
  dialog.showModal();
});

// "Close" button closes the dialog
closeButton.addEventListener("click", () => {
  dialog.close();
});

Ok, trochę się nowego nauczyliśmy, więcej w kolejnych lekcjach…