Skip to content

Latest commit

 

History

History
1294 lines (993 loc) · 42.9 KB

File metadata and controls

1294 lines (993 loc) · 42.9 KB
title useState

useState jest hookiem reactowym, który pozwala na dodanie do komponentu zmiennej stanu.

const [state, setState] = useState(initialState)

Dokumentacja {/reference/}

useState(initialState) {/usestate/}

Wywołaj useState na głównym poziomie komponentu, aby zadeklarować zmienną stanu.

import { useState } from 'react';

function MyComponent() {
  const [age, setAge] = useState(28);
  const [name, setName] = useState('Taylor');
  const [todos, setTodos] = useState(() => createTodos());
  // ...

Przyjęło się nazywać stan [something, setSomething], używając przy tym składni destrukturyzacji tablicy.

Więcej przykładów znajdziesz powyżej.

Parametry {/parameters/}

  • initialState: Wartość, jaką stan ma otrzymać na początku. Może być dowolnego typu, jednak dla funkcji przewidziane jest specjalne zachowanie. Ten argument jest ignorowany po pierwszym renderowaniu komponentu.
    • Jeśli jako argument initialState przekażesz funkcję, będzie ona traktowana jako funkcja inicjalizująca. Musi być "czysta", nie może przyjmować żadnych argumentów i powinna zwracać wartość dla zmiennej stanu. React wywoła twoją funkcję inicjalizującą podczas tworzenia komponentu i przypisze zwróconą przez nią wartość jako stan początkowy. Zobacz przykład powyżej.

Zwracana wartość {/returns/}

useState zwraca tablicę o dokładnie dwóch elementach:

  1. Aktualna wartość stanu. Podczas pierwszego renderowania będzie taka sama jak przekazany do hooka argument initialState.
  2. Funkcja set, która umożliwia zaktualizowanie stanu do innej wartości i wymusza przerenderowanie komponentu.

Zastrzeżenia {/caveats/}

  • useState jest hookiem, więc można go wywoływać tylko na głównym poziomie komponentu lub innego hooka. Nie można go wywołać w pętli lub instrukcji warunkowej. Jeśli masz sytuację, która wymaga pętli lub warunku, stwórz nowy komponent i przenieś do niego ten stan.
  • W Trybie Restrykcyjnym (ang. Strict Mode) React wywoła twoją funkcję inicjalizującą dwukrotnie, aby pomóc ci w zlokalizowaniu niechcianych "nieczystości". To zachowanie tyczy się tylko środowiska deweloperskiego i nie wpływa na produkcję. Jeśli twoja funkcja inicjalizująca jest "czysta" (a powinna być), nie wpłynie to w żaden sposób na logikę twojego komponentu. Wynik z jednego z wywołań tej funkcji zostanie zwyczajnie zignorowany.

Funkcja set, np. setSomething(nextState) {/setstate/}

Funkcja set zwracana przez useState pozwala na zaktualizowanie stanu do innej wartości i wymusza przerenderowanie komponentu. Nową wartość stanu można przekazać bezpośrednio lub można przekazać funkcję, która wyliczy nowy stan na podstawie poprzedniego:

const [name, setName] = useState('Edward');
const [age, setAge] = useState(42);

function handleClick() {
  setName('Taylor');
  setAge(a => a + 1);
  // ...

Parametry {/setstate-parameters/}

Note that if you call a set function while rendering, it must be inside a condition like prevCount !== count, and there must be a call like setPrevCount(count) inside of the condition. Otherwise, your component would re-render in a loop until it crashes. Also, you can only update the state of the currently rendering component like this. Calling the set function of another component during rendering is an error. Finally, your set call should still update state without mutation -- this doesn't mean you can break other rules of pure functions.

  • nextState: Wartość, na jaką chcesz zmienić stan. Może być dowolnego typu, jednak dla funkcji przewidziane jest specjalne zachowanie.
    • Jeśli jako argument nextState przekażesz funkcję, będzie ona traktowana jako funkcja aktualizująca. Musi być "czysta", powinna przyjmować poprzedni stan jako swój jedyny argument i powinna zwracać następną wartość stanu. React umieści twoją funkcję aktualizującą w kolejce i przerenderuje komponent. Podczas kolejnego renderowania React obliczy nowy stan, aplikując kolejno wszystkie zakolejkowane funkcje aktualizujące na poprzednim stanie. Zobacz przykład powyżej.

Zwracana wartość {/setstate-returns/}

Funkcje set nie zwracają żadnej wartości.

Zastrzeżenia {/setstate-caveats/}

  • Funkcja set aktualizuje zmienną stanu tylko dla następnego renderowania. Jeśli spróbujesz odczytać wartość stanu tuż po wywołaniu funkcji set, otrzymasz starą wartość, która istniała przed wywołaniem.

  • Jeśli nowa wartość i aktualny stan są identyczne (na podstawie porównania Object.is), React nie wymusi ponownego renderowania komponentu i jego potomków. Jest to pewna forma optymalizacji. Mimo że czasem React nadal może wywołać twój komponent ponownie przed pominięciem potomków, nie powinno to wpłynąć na logikę działania komponentu.

  • React grupuje aktualizacje stanu. Aktualizuje on ekran po zakończeniu działania wszystkich procedur obsługi zdarzeń i po tym, jak te procedury wywoją odpowiednie funkcje set. Zapobiega to wielokrotnemu renderowaniu komponentu podczas pojedynczego zdarzenia. W rzadkich sytuacjach, kiedy chcesz wymusić wcześniejsze zaktualizowanie ekranu, np. aby odczytać coś z DOM, możesz użyć funkcji flushSync.

  • Funkcja set ma stabilną tożsamość, dlatego często pomija się ją w liście zależności Efektu, jednak uwzględnienie jej nie będzie powodować niepotrzebnych wywołań Efektu. Jeśli linter pozwala ci pominąć tę zależność bez błędów, rób to śmiało. Dowiedz się więcej o usuwaniu zależności Efektów.

  • Wywołanie funkcji set podczas renderowania jest dozwolone tylko w ramach aktualnie renderowanego komponentu. React zignoruje wynik aktualnego renderowania i natychmiast spróbuje wyrenderować go ponownie z nowym stanem. Ten wzorzec jest rzadko stosowany, jednak możesz go użyć, aby zapisać dane z poprzedniego renderowania. Zobacz przykład powyżej.

  • W Trybie Restrykcyjnym (ang. Strict Mode) React wywoła twoją funkcję aktualizującą dwukrotnie, aby pomóc ci w zlokalizowaniu niechcianych "nieczystości". To zachowanie tyczy się tylko środowiska deweloperskiego i nie wpływa na produkcję. Jeśli twoja funkcja aktualizująca jest "czysta" (a powinna być), nie wpłynie to w żaden sposób na logikę twojego komponentu. Wynik z jednego z wywołań tej funkcji zostanie zwyczajnie zignorowany.


Sposób użycia {/usage/}

Dodawanie stanu do komponentu {/adding-state-to-a-component/}

Wywołaj useState na głównym poziomie komponentu, aby zadeklarować jedną lub więcej zmiennych stanu.

import { useState } from 'react';

function MyComponent() {
  const [age, setAge] = useState(42);
  const [name, setName] = useState('Taylor');
  // ...

Przyjęło się, że zmienne stanu nazywamy [something, setSomething], korzystając przy tym z destrukturyzacji tablicy.

useState zwraca tablicę o dokładnie dwóch elementach:

  1. Aktualny stan naszej zmiennej stanu, pierwotnie ustawiony na stan początkowy przekazany jako argument.
  2. Funkcja set, która pozwala zmienić wartość stanu na dowolną inną w odpowiedzi na jakąś interakcję.

Aby zaktualizować to, co jest wyświetlane na ekranie, wywołaj funkcję set, przekazując nowy stan jako argument:

function handleClick() {
  setName('Robin');
}

React zapisze nowy stan, wyrenderuje ponownie twój komponent już z nową wartością, a na koniec zaktualizuje UI.

Wywoływanie funkcji set nie zmienia stanu w trakcie działania kodu:

function handleClick() {
  setName('Robin');
  console.log(name); // Nadal "Taylor"!
}

Wpływa to tylko na to, co useState zwróci przy następnym renderowaniu.

Licznik (liczba) {/counter-number/}

W tym przykładzie zmienna stanu count przechowuje liczbę. Klikanie na przycisk zwiększa tę wartość.

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      Wciśnięto mnie {count} razy
    </button>
  );
}

Pole tekstowe (tekst) {/text-field-string/}

W tym przykładzie zmienna stanu text przechowuje napis. Po wpisaniu czegoś do pola, handleChange odczytuje ostatnią wartość pola tekstowego z elementu DOM, a następnie wywołuje setText w celu ustawienia nowego stanu. Pozwala to na wyświetlenie aktualnego tekstu poniżej pola.

import { useState } from 'react';

export default function MyInput() {
  const [text, setText] = useState('cześć');

  function handleChange(e) {
    setText(e.target.value);
  }

  return (
    <>
      <input value={text} onChange={handleChange} />
      <p>Wpisano: {text}</p>
      <button onClick={() => setText('cześć')}>
        Resetuj
      </button>
    </>
  );
}

Pole wyboru (wartość logiczna) {/checkbox-boolean/}

W tym przykładzie zmienna stanu liked przechowuje wartość logiczną. Kiedy klikniesz na pole wyboru, setLiked zaktualizuje wartość liked na postawie tego, czy pole jest zaznaczone, czy nie. Zmienna liked jest wykorzystywana do wyrenderowanie tekstu pod polem.

import { useState } from 'react';

export default function MyCheckbox() {
  const [liked, setLiked] = useState(true);

  function handleChange(e) {
    setLiked(e.target.checked);
  }

  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={liked}
          onChange={handleChange}
        />
        Lubię to
      </label>
      <p>{liked ? 'Lubisz to' : 'Nie lubisz tego'}.</p>
    </>
  );
}

Formularz (dwie zmienne) {/form-two-variables/}

W komponencie możesz zadeklarować więcej niż jedną zmienną stanu. Każda z nich jest niezależna od pozostałych.

import { useState } from 'react';

export default function Form() {
  const [name, setName] = useState('Taylor');
  const [age, setAge] = useState(42);

  return (
    <>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => setAge(age + 1)}>
        Zwiększ wiek
      </button>
      <p>Cześć, {name}. Masz {age} lat.</p>
    </>
  );
}
button { display: block; margin-top: 10px; }

Aktualizowanie stanu w oparciu o poprzedni stan {/updating-state-based-on-the-previous-state/}

Załóżmy, że wartość age jest obecnie równa 42. Poniższa procedura obsługi zdarzenia wywołuje setAge(age + 1) trzykrotnie:

function handleClick() {
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)
}

Mimo to po jednym kliknięciu wartość age będzie równa 43, a nie 45! Dzieje się tak, ponieważ wywoływanie funkcji set nie aktualizuje zmiennej stanu age w trakcie wywoływania kodu. Tak więc każde setAge(age + 1) tak naprawdę jest tym samym, co setAge(43).

Aby rozwiązać ten problem *możesz przekazać do setAge funkcję aktualizującą zamiast samej wartości:

function handleClick() {
  setAge(a => a + 1); // setAge(42 => 43)
  setAge(a => a + 1); // setAge(43 => 44)
  setAge(a => a + 1); // setAge(44 => 45)
}

W tym przykładzie a => a + 1 jest twoją funkcją aktualizującą. Otrzymuje ona aktualny stan i oblicza na jego podstawie następny stan.

React umieszcza funkcje aktualizujące w kolejce. Następnie, podczas kolejnego renderowania, wywołuje je w takiej samej kolejności:

  1. a => a + 1 otrzyma aktualny stan równy 42 i zwróci następny stan jako 43.
  2. a => a + 1 otrzyma aktualny stan równy 43 i zwróci następny stan jako 44.
  3. a => a + 1 otrzyma aktualny stan równy 44 i zwróci następny stan jako 45.

W tym przypadku nie mamy więcej zakolejkowanych zmian, więc React na koniec zapisze wartość 45 jako aktualny stan.

Przyjęło się, żeby nazywać argument odpowiadający za poprzedni stan używając pierwszej litery nazwy zmiennej stanu, na przykład a dla age. Możesz jednak nazwać go dowolnie, np. prevAge.

React może wywołać twoje funkcje aktualizujące dwukrotnie w środowisku deweloperskim, aby upewnić się, że są one "czyste".

Czy zawsze powinno się używać funkcji aktualizującej? {/is-using-an-updater-always-preferred/}

W internecie można natknąć się na porady, które radzą zawsze pisać setAge(a => a + 1), jeśli następna wartość stanu zależy od poprzedniej. Nie ma w tym nic złego, ale też nie jest to wymagane.

W większości przypadków nie ma różnicy między tymi dwoma podejściami. React zawsze upewnia się, że przy wszelkich intencjonalnych akcjach użytkownika, np. kliknięciach, zmienna stanu age zostanie zaktualizowana jeszcze przed kolejnym kliknięciem. Oznacza to, że nie ma ryzyka, iż procedura obsługi kliknięcia otrzyma "starą" wartość age.

Jeśli jednak wykonujesz kilka aktualizacji stanu przy okazji jednego zdarzenia, funkcje aktualizujące mogą okazać się pomocne. Pomagają one również w sytuacjach, kiedy dostęp do zmiennej stanu jest utrudniony (może się tak zdarzyć po wdrożeniu różnych strategii optymalizujących renderowanie).

Jeśli lubisz spójność w kodzie, możesz zawsze używać funkcji aktualizującej, kiedy nowy stan zależy od poprzedniego. Jeśli jednak nowy stan zależy od poprzedniej wartości innej zmiennej stanu, warto zastanowić się nad połączeniem ich w jeden obiekt i użyciem reduktora (ang. reducer).

Przekazywanie funkcji aktualizującej {/passing-the-updater-function/}

W tym przykładzie przekazujemy funkcję aktualizującą, więc przycisk "+3" zadziała.

import { useState } from 'react';

export default function Counter() {
  const [age, setAge] = useState(42);

  function increment() {
    setAge(a => a + 1);
  }

  return (
    <>
      <h1>Twój wiek: {age}</h1>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <button onClick={() => {
        increment();
      }}>+1</button>
    </>
  );
}
button { display: block; margin: 10px; font-size: 20px; }
h1 { display: block; margin: 10px; }

Przekazywanie nowego stanu bezpośrednio {/passing-the-next-state-directly/}

W tym przykładzie nie przekazujemy funkcji aktualizującej, przez co przycisk "+3" nie działa jak powinien.

import { useState } from 'react';

export default function Counter() {
  const [age, setAge] = useState(42);

  function increment() {
    setAge(age + 1);
  }

  return (
    <>
      <h1>Twój wiek: {age}</h1>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <button onClick={() => {
        increment();
      }}>+1</button>
    </>
  );
}
button { display: block; margin: 10px; font-size: 20px; }
h1 { display: block; margin: 10px; }

Aktualizowanie obiektów i tablic przechowywanych w stanie {/updating-objects-and-arrays-in-state/}

W zmiennej stanu możesz przechowywać obiekty i tablice. W Reakcie stan jest "tylko do odczytu", więc podczas aktualizacji takich zmiennych musisz je zastąpić zamiast modyfikować (mutować). Dla przykładu, jeśli w stanie trzymasz obiekt form, nie aktualizuj go w ten sposób:

// 🚩 Nie modyfikuj obiektu przechowywanego w stanie:
form.firstName = 'Taylor';

Zamiast tego zastąp cały obiekt poprzez stworzenie całkiem nowego:

// ✅ Zastąp stan nowym obiektem
setForm({
  ...form,
  firstName: 'Taylor'
});

Aby dowiedzieć się więcej na ten temat, przeczytaj rozdziały pt. Aktualizowanie obiektów w stanie i Aktualizowanie tablic w stanie.

Formularz (obiekt) {/form-object/}

W tym przykładzie zmienna stanu form przechowuje obiekt. Każda kontrolka formularza ma przypisaną procedurę obsługi zmiany wartości, która wywołuje setForm z nowym stanem całego formularza. Składnia { ...form } daje nam pewność, że obiekt w stanie zostanie zastąpiony, a nie tylko zmodyfikowany.

import { useState } from 'react';

export default function Form() {
  const [form, setForm] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com',
  });

  return (
    <>
      <label>
        Imię:
        <input
          value={form.firstName}
          onChange={e => {
            setForm({
              ...form,
              firstName: e.target.value
            });
          }}
        />
      </label>
      <label>
        Nazwisko:
        <input
          value={form.lastName}
          onChange={e => {
            setForm({
              ...form,
              lastName: e.target.value
            });
          }}
        />
      </label>
      <label>
        E-mail:
        <input
          value={form.email}
          onChange={e => {
            setForm({
              ...form,
              email: e.target.value
            });
          }}
        />
      </label>
      <p>
        {form.firstName}{' '}
        {form.lastName}{' '}
        ({form.email})
      </p>
    </>
  );
}
label { display: block; }
input { margin-left: 5px; }

Formularz (zagnieżdżony obiekt) {/form-nested-object/}

W tym przykładzie stan jest nieco bardziej zagnieżdżony. Kiedy aktualizujesz zagnieżdżony stan, musisz stworzyć kopię tego obiektu, jak również wszystkich obiektów wyżej, które go "zawierają". Przeczytaj rozdział pt. Aktualizowanie zagnieżdżonych obiektów, aby dowiedzieć się więcej.

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    setPerson({
      ...person,
      name: e.target.value
    });
  }

  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }

  function handleCityChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: e.target.value
      }
    });
  }

  function handleImageChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        image: e.target.value
      }
    });
  }

  return (
    <>
      <label>
        Imię i nazwisko:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Tytuł:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        Miasto:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Zdjęcie:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' autorstwa '}
        {person.name}
        <br />
        (mieszka w {person.artwork.city})
      </p>
      <img 
        src={person.artwork.image} 
        alt={person.artwork.title}
      />
    </>
  );
}
label { display: block; }
input { margin-left: 5px; margin-bottom: 5px; }
img { width: 200px; height: 200px; }

Lista (tablica) {/list-array/}

W tym przykładzie zmienna stanu todos przechowuje tablicę. Każda procedura obsługi kliknięcia na przyciskach wywołuje setTodos z następną wersją tej tablicy. Składnia [...todos], todos.map() oraz todos.filter() daje nam pewność, że tablica w stanie zostanie zastąpiona, a nie tylko zmodyfikowana.

import { useState } from 'react';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';

let nextId = 3;
const initialTodos = [
  { id: 0, title: 'Kupić mleko', done: true },
  { id: 1, title: 'Zjeść bigos', done: false },
  { id: 2, title: 'Zaparzyć herbatę', done: false },
];

export default function TaskApp() {
  const [todos, setTodos] = useState(initialTodos);

  function handleAddTodo(title) {
    setTodos([
      ...todos,
      {
        id: nextId++,
        title: title,
        done: false
      }
    ]);
  }

  function handleChangeTodo(nextTodo) {
    setTodos(todos.map(t => {
      if (t.id === nextTodo.id) {
        return nextTodo;
      } else {
        return t;
      }
    }));
  }

  function handleDeleteTodo(todoId) {
    setTodos(
      todos.filter(t => t.id !== todoId)
    );
  }

  return (
    <>
      <AddTodo
        onAddTodo={handleAddTodo}
      />
      <TaskList
        todos={todos}
        onChangeTodo={handleChangeTodo}
        onDeleteTodo={handleDeleteTodo}
      />
    </>
  );
}
import { useState } from 'react';

export default function AddTodo({ onAddTodo }) {
  const [title, setTitle] = useState('');
  return (
    <>
      <input
        placeholder="Dodaj zadanie"
        value={title}
        onChange={e => setTitle(e.target.value)}
      />
      <button onClick={() => {
        setTitle('');
        onAddTodo(title);
      }}>Dodaj</button>
    </>
  )
}
import { useState } from 'react';

export default function TaskList({
  todos,
  onChangeTodo,
  onDeleteTodo
}) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <Task
            todo={todo}
            onChange={onChangeTodo}
            onDelete={onDeleteTodo}
          />
        </li>
      ))}
    </ul>
  );
}

function Task({ todo, onChange, onDelete }) {
  const [isEditing, setIsEditing] = useState(false);
  let todoContent;
  if (isEditing) {
    todoContent = (
      <>
        <input
          value={todo.title}
          onChange={e => {
            onChange({
              ...todo,
              title: e.target.value
            });
          }} />
        <button onClick={() => setIsEditing(false)}>
          Zapisz
        </button>
      </>
    );
  } else {
    todoContent = (
      <>
        {todo.title}
        <button onClick={() => setIsEditing(true)}>
          Edytuj
        </button>
      </>
    );
  }
  return (
    <label>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={e => {
          onChange({
            ...todo,
            done: e.target.checked
          });
        }}
      />
      {todoContent}
      <button onClick={() => onDelete(todo.id)}>
        Usuń
      </button>
    </label>
  );
}
button { margin: 5px; }
li { list-style-type: none; }
ul, li { margin: 0; padding: 0; }

Pisanie zwięzłej logiki aktualizującej za pomocą Immera {/writing-concise-update-logic-with-immer/}

Jeśli aktualizowanie tablic i obiektów bez modyfikacji wydaje ci się żmudne, możesz użyć biblioteki takiej jak Immer i zmniejszyć ilość powtarzalnego kodu. Immer umożliwia pisanie zwięzłego kodu, który wygląda jak modyfikacja obiektów, ale w rzeczywistości wykonuje on niemutujące aktualizacje:

import { useState } from 'react';
import { useImmer } from 'use-immer';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Wielkie brzuchy', seen: false },
  { id: 1, title: 'Krajobraz powierzchni księżyca', seen: false },
  { id: 2, title: 'Terakotowa armia', seen: true },
];

export default function BucketList() {
  const [list, updateList] = useImmer(initialList);

  function handleToggle(artworkId, nextSeen) {
    updateList(draft => {
      const artwork = draft.find(a =>
        a.id === artworkId
      );
      artwork.seen = nextSeen;
    });
  }

  return (
    <>
      <h1>Obowiązkowa sztuka</h1>
      <h2>Lista obrazów, które muszę zobaczyć:</h2>
      <ItemList
        artworks={list}
        onToggle={handleToggle} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}
{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}

Unikanie ponownego tworzenia stanu początkowego {/avoiding-recreating-the-initial-state/}

React zapisuje stan początkowy tylko jeden raz, a przy kolejnych renderowaniach zwyczajnie go ignoruje.

function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos());
  // ...

Mimo że wynik funkcji createInitialTodos() jest używany tylko podczas pierwszego renderowania, i tak jest ona wywoływana przy każdym kolejnym renderowaniu. Czasami może być to problem, jeśli podczas działania tworzy ona dużą tablicę lub wykonuje kosztowne obliczenia.

Można sobie z tym poradzić przekazując do useState funkcję inicjalizującą:

function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos);
  // ...

Zwróć uwagę, że przekazaliśmy tutaj createInitialTodos, która jest funkcją, a nie createInitialTodos(), które jest wynikiem jej wywołania. Jeśli do useState przekażesz jakąś funkcję, React wywoła ją tylko podczas inicjalizacji.

React może wywołać twoją funkcję inicjalizującą dwukrotnie w środowisku deweloperskim, aby sprawdzić, czy jest ona "czysta".

Przekazywanie funkcji inicjalizującej {/passing-the-initializer-function/}

W tym przykładzie przekazujemy funkcję inicjalizującą, więc createInitialTodos jest wywoływana tylko podczas inicjalizacji. Nie wywołuje się podczas kolejnych renderowań, np. po wpisaniu tekstu do pola formularza.

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;
}

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]);
      }}>Dodaj</button>
      <ul>
        {todos.map(item => (
          <li key={item.id}>
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}

Przekazywanie wartości początkowej bezpośrednio {/passing-the-initial-state-directly/}

W tym przykładzie nie przekazujemy funkcji inicjalizującej, więc funkcja createInitialTodos jest wywoływana przy każdym renderowaniu, np. kiedy wpiszemy coś w pole formularza. Nie robi to żadnej różnicy w tym, co zostanie wyświetlone na ekranie, jednak taki kod jest mnie efektywny.

import { useState } from 'react';

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

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]);
      }}>Dodaj</button>
      <ul>
        {todos.map(item => (
          <li key={item.id}>
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}

Resetowanie stanu za pomocą właściwości key {/resetting-state-with-a-key/}

W większości przypadków z właściwością key spotkasz się tylko przy okazji renderowania list. Czasami jednak służy ona do czegoś innego.

Przekazując inną wartość key do komponentu możesz zresetować jego stan. W poniższym przykładzie przycisk resetujący ustawia zmienną stanu version, którą możemy przekazać jako właściwość key do Form. Kiedy zmieni się key, React stworzy komponent Form od nowa (razem ze wszystkimi potomkami), dzięki czemu ich stan zostanie zresetowany.

Aby dowiedzieć się więcej, przeczytaj rozdział pt. Zachowywanie i resetowanie stanu.

import { useState } from 'react';

export default function App() {
  const [version, setVersion] = useState(0);

  function handleReset() {
    setVersion(version + 1);
  }

  return (
    <>
      <button onClick={handleReset}>Resetuj</button>
      <Form key={version} />
    </>
  );
}

function Form() {
  const [name, setName] = useState('Taylor');

  return (
    <>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <p>Cześć, {name}.</p>
    </>
  );
}
button { display: block; margin-bottom: 20px; }

Przechowywanie informacji z poprzednich renderowań {/storing-information-from-previous-renders/}

Stan zazwyczaj aktualizujemy w procedurach obsługi zdarzeń (ang. event handlers). W rzadkich przypadkach możemy chcieć zmienić stan w odpowiedzi na renderowanie - na przykład, żeby zmienić stan przy zmianie właściwości.

Zwykle jednak nie ma potrzeby tak robić:

Są jednak sytuację, w których żadna z powyższych reguł nie ma zastosowania. Można wtedy aktualizować stan na podstawie wartości, które już zostały wyrenderowane, wywołując funkcję set w trakcie renderowania komponentu.

Oto przykład. Komponent CountLabel wyświetla wartość przekazanej do niego właściwości count:

export default function CountLabel({ count }) {
  return <h1>{count}</h1>
}

Załóżmy, że chcesz wyświetlić informację, czy licznik został zwiększony, czy zmniejszony od ostatniej zmiany. Właściwość count nie mówi ci tego w żaden sposób - musisz zatem jakoś śledzić jej poprzednią wartość. W takiej sytuacji należy dodać kolejne zmienne stanu: jedną prevCount do śledzenia wartości oraz drugą trend do przechowywania informacji o kierunku tej zmiany. Teraz wystarczy porównać prevCount z count i jeśli nie są równe, zaktualizować zarówno prevCount, jak i trend. Dzięki temu możliwe będzie wyświetlenie obydwu wartości oraz określenie, jak zmieniły się one od ostatniego renderowania.

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

export default function App() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        Zwiększ
      </button>
      <button onClick={() => setCount(count - 1)}>
        Zmniejsz
      </button>
      <CountLabel count={count} />
    </>
  );
}
import { useState } from 'react';

export default function CountLabel({ count }) {
  const [prevCount, setPrevCount] = useState(count);
  const [trend, setTrend] = useState(null);
  if (prevCount !== count) {
    setPrevCount(count);
    setTrend(count > prevCount ? 'zwiększa się' : 'zmniejsza się');
  }
  return (
    <>
      <h1>{count}</h1>
      {trend && <p>Licznik {trend}</p>}
    </>
  );
}
button { margin-bottom: 10px; }

Zwróć uwagę, że jeśli wywołujesz funkcję set podczas renderowania, musi się to odbywać w warunku prevCount !== count, w którym to również wywołujesz setPrevCount(count). W przeciwnym wypadku komponent renderowałby się ponownie w nieskończoność, co doprowadziłoby do zawieszenia aplikacji. Pamiętaj, że możesz w ten sposób aktualizować stan tylko aktualnie renderowanego komponentu. Wywoływanie funkcji set pochodzącej z innego komponentu podczas renderowania byłoby błędem. I wreszcie, pamiętaj, że wywołanie funkcji set powinno aktualizować stan bez jego mutowania -- to, że obsługujemy tu przypadek specjalny, nie oznacza, że możemy łamać inne zasady czystych funkcji.

Powyższy schemat działania może wydawać się trudny do zrozumienia i generalnie lepiej go unikać. Mimo wszystko jest on lepszy niż aktualizowanie stanu w efekcie. Kiedy wywołujesz funkcję set podczas renderowania, React wyrenderuje go ponownie tuż po tym, jak zwróci on coś za pomocą instrukcji return, ale jeszcze przed wyrenderowaniem potomków. Dzięki temu komponenty potomne nie będą renderowały się dwa razy. Pozostała część funkcji komponentu nadal będzie wywołana (a wynik zostanie "wyrzucony do kosza"), dlatego jeśli taki warunek znajduje się pod wywołaniami hooków, możesz dopisać do niego return;, aby zakończyć renderowanie wcześniej.


Znane problemy {/troubleshooting/}

Aktualizuję wartość stanu, ale w konsoli wyświetla mi się stan poprzedni {/ive-updated-the-state-but-logging-gives-me-the-old-value/}

Wywołanie funkcji set nie powoduje zmiany stanu w trakcie wykonywania kodu:

function handleClick() {
  console.log(count);  // 0

  setCount(count + 1); // Zażądaj przerenderowania z wartością 1
  console.log(count);  // Nadal 0!

  setTimeout(() => {
    console.log(count); // Również 0!
  }, 5000);
}

Dzieje się tak dlatego, że stan zachowuje się jak migawka aparatu (ang. snapshot). Aktualizacja stanu wysyła żądanie przerenderowania komponentu z nową wartością, lecz nie wpływa na zmienną javascriptową count w aktualnie wykoływanym fragmencie kodu.

Jeśli potrzebujesz od razu skorzystać z nowej wartości stanu, przed przekazaniem jej do funkcji set zapisz ją do zmiennej lokalnej:

const nextCount = count + 1;
setCount(nextCount);

console.log(count);     // 0
console.log(nextCount); // 1

Aktualizuję wartość stanu, ale ekran się nie odświeża {/ive-updated-the-state-but-the-screen-doesnt-update/}

React zignoruje aktualizację stanu, jeśli nowa wartość jest identyczna z poprzednim stanem (na podstawie porównania Object.is). Zwykle przyczyną jest bezpośrednia mutacja obiektu lub tablicy przechowywanych w stanie:

obj.x = 10;  // 🚩 Źle: mutacja istniejącego obiektu
setObj(obj); // 🚩 Nic się nie dzieje

Zmutowaliśmy istniejący obiekt obj, a następnie przekazaliśmy go do setObj, dlatego React zignorował tę aktualizację. Aby naprawić ten błąd, należy zawsze zastępować obiekty i tablice przechowywane w stanie, zamiast je mutować:

// ✅ Dobrze: tworzymy nowy obiekt
setObj({
  ...obj,
  x: 10
});

Dostaję błąd: "Too many re-renders" {/im-getting-an-error-too-many-re-renders/}

Możesz natknąć się na błąd o treści: Too many re-renders. React limits the number of renders to prevent an infinite loop. (pol. Zbyt wiele ponownych renderowań. React ogranicza liczbę renderowań, aby zapobiec nieskończonej pętli.). Zwykle oznacza to, że aktualizujemy stan bezwarunkowo podczas renderowania, więc komponent wchodzi w pętlę: renderuje, ustawia stan (co wymusza ponowne wyrenderowanie), renderuje, ustawia stan (co wymusza ponowne wyrenderowanie) itd. Bardzo często przyczyną jest błąd w definicji procedury obsługi zdarzenia:

// 🚩 Źle: wywołuje procedurę obsługi zdarzenia podczas renderowania
return <button onClick={handleClick()}>Kliknij mnie</button>

// ✅ Dobrze: przekazuje procedurę obsługi zdarzenia
return <button onClick={handleClick}>Kliknij mnie</button>

// ✅ Dobrze: przekazuje funkcję "inline"
return <button onClick={(e) => handleClick(e)}>Kliknij mnie</button>

Jeśli nie możesz namierzyć przyczyny tego błędu, kliknij na strzałkę obok treści błędu i przejrzyj stos JavaScriptu w celu znalezienia trefnego wywołania funkcji set.


Moja funkcja inicjalizująca lub aktualizująca jest uruchamiana dwa razy {/my-initializer-or-updater-function-runs-twice/}

W Trybie Restrykcyjnym (ang. Strict Mode) React wywołuje niektóre funkcje dwukrotnie:

function TodoList() {
  // Ta funkcja komponentu będzie wywoływana dwukrotnie przy każdym renderowaniu.

  const [todos, setTodos] = useState(() => {
    // Ta funkcja inicjalizująca zostanie wywołana dwukrotnie podczas tworzenia komponentu.
    return createTodos();
  });

  function handleClick() {
    setTodos(prevTodos => {
      // Ta funkcja aktualizująca zostanie wywołana dwukrotnie przy każdym kliknięciu.
      return [...prevTodos, createTodo()];
    });
  }
  // ...

To zachowanie jest celowe i nie powinno popsuć działania aplikacji.

Takie zachowanie, wystepujące tylko w środowisku deweloperskim, pozwala na sprawdzenie "czystości" komponentów. React wykorzysta wynik z jednego z wywołań tych funkcji, a zignoruje drugi. Dopóki twój komponent oraz funkcje inicjalizujące i aktualizujące są czyste, nic nie powinno się popsuć. Jeśli jednak któraś z nich nie jest czysta, taki mechanizm pomoże ci ją znaleźć i naprawić.

Dla przykładu, poniższa nieczysta funkcja aktualizująca mutuje tablicę przechowywaną w stanie:

setTodos(prevTodos => {
  // 🚩 Błąd: mutacja stanu
  prevTodos.push(createTodo());
});

Z racji tego, że React wywołuje funkcje aktualizujące dwukrotnie, zauważysz, że zadanie zostanie dodane do listy TODO dwa razy, co będzie wskazywało na błąd. W tym przykładzie możemy to naprawić zastępując tablicę zamiast ją mutować:

setTodos(prevTodos => {
  // ✅ Dobrze: zastępujemy nowym stanem
  return [...prevTodos, createTodo()];
});

Teraz, kiedy nasza funkcja aktualizująca jest czysta, wywołanie jej dwukrotnie nie spowoduje żadnych różnic w działaniu komponentu. To w taki sposób React pomaga ci znajdować błędy. Tylko komponent oraz funkcje initializujące i aktualizujące muszą być czyste. Procedury obsługi zdarzeń nie muszą być czyste, a React nigdy nie wywoła ich dwukrotnie.

Aby dowiedzieć się więcej, przeczytaj rozdział pt. Czyste komponenty.


Próbuję zapisać w stanie funkcję, ale zamiast tego moja funkcja jest wywoływana {/im-trying-to-set-state-to-a-function-but-it-gets-called-instead/}

Nie możesz przypisać funkcji do stanu w taki sposób:

const [fn, setFn] = useState(someFunction);

function handleClick() {
  setFn(someOtherFunction);
}

Ponieważ przekazujesz funkcję, React zakłada, że someFunction jest funkcją inicjalizującą i że someOtherFunction jest funkcją aktualizującą, więc próbuje je wywołać i zapisać wynik ich działania. Aby faktycznie zapisać funkcję, w obydwóch przypadkach musisz poprzedzić je () =>. Tylko wtedy React zapisze przekazywane przez ciebie funkcje.

const [fn, setFn] = useState(() => someFunction);

function handleClick() {
  setFn(() => someOtherFunction);
}