• Post last modified:07/04/2020
  • Reading time:8 mins read
  • Post comments:0 Komentarzy

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 🙂

Kamil Józwik

Frontend developer👨‍💻 Autor kursów na frontschool.pl, małego narzędzia dla programistów - frontbook.dev oraz kolejnych postów na tym blogu. Ulubiony stos technologiczny to React wraz z TypeScript oraz wszystko, co "nowe" w tym pięknym dynamicznym front end-owym świecie 🙂
Subscribe
Powiadom o
guest
0 Comments
Inline Feedbacks
View all comments