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…