Dzisiejszy post zostanie poświęcony tematowi React render props , czyli jednemu ze sposobów na re-używanie wspólnej logiki w komponentach. Dzięki temu tą samą czynność może powtórzyć w wielu miejscach i zmienić tylko sposób wyświetlania wyniku tej czynności (logiki). Omawiane zagadnienie jest alternatywą dla bardziej znanego podejścia, tj. Higher-order component (HOC).
Uwaga
Post powstał jeszcze w czasach, gdy nikt nie słyszał jeszcze o hookach. W związku z tym wszystkie przykłady zostały oparte na komponentach klasowych . Mam nadzieję, że w najbliższym czasie uda mi się zaktualizować ten wpis. Jednakże należy pamiętać, iż klasy są w dalszym ciągu poprawnym sposobem na tworzenie komponentów.
Czym jest „react render props”?
Już sama nazwa tej techniki – render props, mówi nam całkiem sporo na jej temat. Główne założenie tej metody polega na tym, iż w wielu miejscach naszej aplikacji chcemy mieć zaimplementowaną pewną powtarzalną logikę. Może to być np. pobieranie danych z API, śledzenie ruchów myszki użytkownika, wykonywanie operacji matematycznych, formatowanie/generowanie tekstu, itp. Jednak za każdym razem chcemy nieco inaczej interpretować wyniki tych operacji, poprzez np. wyświetlanie ich w różnej formie w UI, dokonywać na nich dodatkowych operacji czy zapisywać w różnych miejscach.
W takim przypadku tworzymy jeden komponent React’owy w którym implementujemy tylko i wyłącznie zdefiniowaną przez nas logikę i w metodzie render()
tego komponentu, zamiast zwracać elementy DOM’u które mają zostać wyrenderowane w przeglądarce, zwracamy wynik działania naszych operacji.
Uwaga – cały czas korzystamy z React, więc w metodzie render musimy zwrócić jeden z obsługiwanych typów:
- element React’owy
- tablica lub fragment
- portal
- string/number
- bool
- null
Rozszyfrowaliśmy więc pierwszą część nazwy – render.
Co dalej z tymi danymi – przecież pracujemy w React, więc chcemy wyświetlić coś w przeglądarce naszego użytkownika. Tworzymy więc teraz kolejne komponenty, których zadaniem będzie przejęcie danych od „mądrego” komponentu z logiką i wyrenderowanie ich w przeglądarce. W tym celu wywołujemy „mądry” komponent, podając mu jako jeden z props’ów funkcję o nazwie.. render.
Mamy więc już całość – render props.
Warto w tym momencie dopowiedzieć, iż czasami można zetknąć się z pojęciem „children as a function”. Jest to dokładnie ten sam mechanizm, w którym jedynie zmienia się nomenklatura. Zamieniamy nazwę props’a „render” na „children”. Używanie nazwy „render” jest jednak w tym przypadku najłatwiejsze do zrozumienia.
Teraz możemy stworzyć wiele komponentów korzystających z logiki „mądrego” komponentu.
<ComponentWithLogic render={data => ( <h1>Hello {data.target}</h1> )}/>
Wszystko to brzmi teraz na pewno bardzo zawile i skomplikowanie, więc przejdziemy przez trzy przykłady. Mam nadzieję, że rozwieją wszelkie wątpliwości związane z tym czym jest i jak korzystać z render props.
Przykład 1: Hello Props
Pierwszy przykład będzie bardzo prosty i pozwoli nam w końcu zobaczyć trochę działającego kodu. Nasz re-używalny komponent będzie posiadał funkcjonalność powitania użytkownika. Będzie on wywoływany do tego, aby witać podane mu (poprzez props) osoby.
Zacznijmy od stworzenia ReusableComponent (czyli komponentu z zaimplementowaną re-używalną logiką)
import React from "react"; const GREET = "Hello"; class ReusableComponent extends React.Component { render() { return <div>{this.props.render({ greet: GREET })}</div>; } } export default ReusableComponent;
this.props.render()
jest funkcją przekazaną przez inny komponent. Funkcja ta powinna zwrócić komponent React’owy. Ten komponent będzie wyrenderowany wewnątrz elementu div
.
W naszym przypadku przekazujemy greet
jako parametr funkcji render prop
. Dzięki temu możemy użyć jej poza definiowanym komponentem (czyli gdzieś poza komponentem ReusableComponent). Tutaj jest to stała wartość. Jednakże możemy wykonywać tutaj wiele akcji które np. formują stan komponentu i przekazać ten stan poza komponent.
Wykorzystajmy teraz nasz komponent:
import React from "react"; import ReactDOM from "react-dom"; import ReusableComponent from "./ReusableComponent"; const Name = "Mario"; const SayHello = () => ( <ReusableComponent render={({ greet }) => ( // render prop <span> {greet}, {Name} </span> )} /> ); const rootElement = document.getElementById("root"); ReactDOM.render(<SayHello />, rootElement);
W wyniku wykonania powyższego kodu, w DOM zauważymy:
<div> <span>Hello, Mario</span> </div>
Przykład 2: Mouse tracking
Omówmy teraz przykład, który jest bardzo często prezentowany jako przykład wykorzystania render props
. Stworzymy komponent, który będzie posiadał logikę, dzięki której jest w stanie śledzić pozycję naszego kursora na ekranie. Nie interesuje go za bardzo jak ta informacja będzie wyświetlana. Jedyne co interesuje ten komponent to śledzenie myszki i przekazywanie tej informacji. Inne komponenty mogą z niego korzystać i w zależności od potrzeby, renderować to w specyficzny dla siebie sposób.
import React from "react"; import ReactDOM from "react-dom"; class MouseTracker extends React.Component { // komponent z powtarzalną logiką state = { x: 0, y: 0 }; handleMouseMove = event => { // pozycja kursora jest przechowywana w stanie komponentu this.setState({ x: event.clientX, y: event.clientY }); }; render() { return ( <div style={{ height: "100%" }} onMouseMove={this.handleMouseMove}> {/* poniżej render prop - przekazujemy stan komponentu jako argument funkcji render */} {this.props.render(this.state)} </div> ); } } class App extends React.Component { render() { return ( <div style={{ height: "100vh" }}> <MouseTracker render={({ x, y }) => ( // Render prop daje nam dostęp do stanu komponentu MouseTracker // i możemy robić z tymi wartościami na co mamy ochote <h1> Pozycja myszki: ({x}, {y}) </h1> )} /> </div> ); } } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
W podanym wyżej przykładzie, komponent który umożliwia nam śledzenie kursora zapisuje aktualną pozycję myszki w swoim stanie i udostępnia ten stan poprzez funkcję render
. Wartość tego stanu jest odczytywana w komponencie, który wywołuje komponent MouseTracker
(tutaj: App
) i decyduje o tym w jaki sposób takie informacje wyświetlić użytkownikowi w UI.
Przykład 3: List fetching
Ostatni przykład będzie trochę bardziej przypominał sytuację z codziennego życia developera. Będziemy pobierać elementy listy z API i renderować je na ekranie. Podczas pobierania danych wyświetlamy informację o tym, iż operacja jest w trakcie wykonywania.
import React from "react"; import { render } from "react-dom"; // komponent z logiką, który może być użyty wielokrotnie class List extends React.Component { // stan będzie przekazany jako argument funkcji render state = { list: [], isLoading: false }; // logika komponentu - pobranie elementów do wyświetlenia _fetch = async () => { const res = await fetch(this.props.url); const json = await res.json(); this.setState({ list: json, isLoading: false }); }; // jak najbardziej możemy korzystać z lifecycles componentDidMount() { this.setState({ isLoading: true }, this._fetch); } render() { // wywołanie render props i przekazanie stanu jako parametru return this.props.render(this.state); } } const App = () => ( <div> { /* Wyświetlenie pierwszej listy */} <List url="https://jsonplaceholder.typicode.com/users" render={({ list, isLoading }) => ( <div> <h2>Pierwsa lista - Users List</h2> {isLoading && <h2>Loading...</h2>} <ul> {list.length > 0 && list.map(user => <li key={user.id}>{user.name}</li>)} </ul> </div> )} /> <hr /> { /* Wyświetlenei drugiej listy */} <List url="https://jsonplaceholder.typicode.com/posts" render={({ list, isLoading }) => ( <div> <h2>Druga lista - Posts Titles</h2> {isLoading && <h2>Loading...</h2>} <ul> {list.length > 0 && list.map(post => <li key={post.id}>{post.title}</li>)} </ul> </div> )} /> </div> ); const rootElement = document.getElementById("root"); render(<App />, rootElement);
Zasada działania jest bardzo podobna do przykładu drugiego. Również mamy tutaj „mądry” komponent, który potrafi pobierać elementy listy, następnie przy jego pomocy renderujemy dwie zupełnie inne listy.
Podsumowanie
Podsumujmy teraz w dwóch słowach dzisiejszy wpis. React render props jest bardzo przydatną techniką, dzięki której możemy współdzielić logikę jednego komponentu z innymi komponentami. Render props są bardzo podobne w swoim działaniu do HOC – wybranie jednej, bądź drugiej techniki zależy od preferencji developera. Ja osobiście jestem zwolennikiem używania render props nad HOC. Mamy tutaj do czynienia z normalnymi komponentami oraz funkcjami, bez potrzeby używania dekoratorów oraz pisania kodu boilerplate dla HOC. Dodatkowo wszystko tutaj dzieje się w metodzie render() komponentu, więc możemy w pełni korzystać z React lifecycle – tak jak robimy to na co dzień z każdym komponentem.
Dodatkowym, niepodważalnym argumentem jak dla mnie jest fakt, iż nawet Michal Jackson uważa, że jest w stanie przepisać każdy HOC na komponent używający render props!
Jeżeli mi nie wierzycie – polecam zajrzeć tutaj 🙂