Poznajemy różne sposoby na tzw. conditional rendering w aplikacjach React, przypominamy sobie pewne zasady. Do dzieła.

Ok, przypomnijmy sobie kilka zasad:

  • Każdy prop komponentu, zmienna reaktywna, funkcja lub obiekt utworzone w komponencie, ale nie wewnątrz hooka, to potencjalna zależność
  • Każda funkcja utworzona na poziomie komponentu będzie definiowana od zera przy każdym re-renderze (chyba że użyjemy useCallback)
  • Każda zmienna, stała, każdy obiekt, tablica lub inna wartość tworzona/definiowana na poziomie komponentu będzie tworzona od zera przy każdym re-renderze (chyba że użyjemy useMemo)
  • Hooków reacta nie możemy używać poza komponentami, musimy ich używać na poziomie top-level, nie mogą być zagnieżdżone w blokach if
  • Od tej zasady jest jeden wyjątek – funkcje custom hooks, zaczynające się od słowa use, mogą używać hooków reacta, choć nie są komponentami funkcyjnymi
  • Funkcyjne komponenty nie mogą mieć słowa async, callback dla useEffect również nie może mieć słowa async
  • Wewnątrz callbacku useEffect możemy używać funkcji asnyc (jak fetch) albo definiować funkcje z async
  • Funkcje zdefiniowane w useEffect nie będą definiowane przy każdym re-renderze tylko przy każdym użyciu useEffect
  • To samo tyczy się zmiennych, stałych i innych takich definiowanych wewnątrz useEffect
  • Initial state przekazany do useState jest odpalany przy renderze i ignorowany przy re-renderach
  • Jeżeli przekazujemy do useState wywołanie funkcji tworzącej initial state to przy każdym re-renderze ta funkcja też jest odpalana
  • Jeżeli przekazujemy do useState pointer na funkcję tworzącą initial state to wszystko jest odpalane jak należy
  • Gdy robimy return z kodem JSX to już nie możemy w żaden sposób robić tam ifów, elsów

Ok, najpierw poznajmy o co chodzi z initializer function useState. Potężna funkcja tworząca stan początkowy:

import { useState } from 'react';

function createInitialTodos() {
  const initialTodos = [];
  for (let i = 0; i < 50; i++) {
    initialTodos.push({
      id: i,
      text: 'Item ' + (i + 1)
    });
  }
  return initialTodos;
}

Komponent ToDo list traktujący z pointerem na tę funkcję jako initial state:

export default function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos);
  const [text, setText] = useState('');

  return (
    <>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={() => {
        setText('');
        setTodos([{
          id: todos.length,
          text: text
        }, ...todos]);
      }}>Add</button>
      <ul>
        {todos.map(item => (
          <li key={item.id}>
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}

To jest ok i działa jak należy. Gdybyśmy jednak wrzucili tam zawołanie funkcji to byłoby źle:

export default function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos());
  const [text, setText] = useState('');

Byłoby źle, bo każda zmiana text wywołująca re-render odpalałaby createInitialTodos.

Ok, a teraz renderowanie warunkowe. Dużo tych zasad, więc może nam się wydawać, że taki kod nie jest ok:

const SomeComponent = () => {
    const {data, isLoading, error} = useFetch()
    if (isLoading) {
        return <span>loading</span>
    }
    if (error) {
        return <span>{error.message}</span>
    }
    return <>{data}</>
}

Jest jak najbardziej ok, to jest coś, co w React robić możemy. Dodam, że useFetch to jakiś custom hook, poznamy niedługo.

Oczywiście możemy robić conditional rendering w JSX:

const SomeComponent = () => {
    const {data, isLoading, error} = useFetch()
        return (
            <>
                {data && data}
                {isLoading && <span>loading</span>}
                {error && <span>{error.message}</span>}
            </>
        )
}

Teraz pytanie, kiedy:

  • Gdy mamy taki warunek, który zwraca coś zupełnie innego to nie warto, lepiej zrobić to jak powyżej
  • Gdy mamy taki warunek, taką sytuację, że zwracamy mniej więcej ten sam komponent, ale wewnątrz coś będzie inaczej w zależności od czegoś – to warto

Czyli przykład, kiedy warto używać ifów i zwracać różny JSX to nasz SomeComponent w wersji pierwszej. Przykład, kiedy opłaca się robić conditional rendering w jednym returnie, jednym kodzie JSX podaję poniżej:

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

Tu absolutnie nie ma sensu robić tego w żaden inny sposób. Dodam, że mamy tu ternary operator oraz logic AND (jeżeli show prawdziwe, pokaż to po prawej).

Dużo rzadziej używamy logical or:

function LogicalOR({name, labelText}) {

  return (
    <>       
        /* Logical 'OR' Operator*/
         {labelText || name}      
    </>
  )
}

export default LogicalOR

Zasada jest taka, że jak labelText jest, to pokaż to, nie pokazuj już tego po prawej, ucinaj (short circuit evaluation). No jeśli jednak nie ma labelText to pokaż to, co masz po prawej, o ile masz.

Ok, to by było na tyle, więcej Reacta niedługo…