Metoda setState() w React i React Native

W React oraz w React Native wszystkie dane, które mają zostać wyświetlone lub mają realny wpływ na wyświetlaną treść, powinny zostać zapisywane w stanie komponentu. Jest to o tyle ważne, ponieważ aby zaktualizować takie wartości musimy wywołać metodę setState(), która spowoduje ponowne renderowanie się naszego komponentu. Jednak musimy pamiętać o kilku bardzo ważnych kwestiach.

Metoda setState() jest podstawową funkcją służącą do aktualizacji interfejsu użytkownika. Przyjmuje ona nowe dane, które zostają ustawione w kolejce do zaktualizowania interfejsu, ale najistotniejszą kwestią jest to, że zmiana stanu nie musi wcale nastąpić natychmiast. Może ona nastąpić dopiero po jakimś bliżej nieokreślonym czasie więc można ją traktować jako swego rodzaju obietnicę tej czynności. Jednak tutaj uwidacznia się najważniejsza sprawa, a mianowicie...

setState() nie zwraca Promise

Zdążyłem zauważyć, że bardzo dużo osób traktuje metodę setState() w React lub w React Native jakby miała ona zwrócić obiekt Promise. Jest to podstawowy błąd, ponieważ pomimo tego, że metoda ta jest asynchroniczna, to z całą pewnością nie możemy czekać na jej wykonanie, stawiając przed nią słowa kluczowego await. Może to doprowadzić prędzej czy później do niespodziewanych problemów w działaniu aplikacji.. Zobaczmy więc przykładowy, błędny kod:

class App extends Component {
  state = { value: 0 };

  letsDoIt = async () => {
    // this.state.value ma wartość początkową 0
    await this.setState({ value: 1 });

    // ten warunek nie zawsze zostanie spełniony
    if(this.state.value === 1) {
      return 'Wygrałem na loterii :P';
    }
  }
}

Widzimy więc asynchroniczną funkcję letsDoIt(), która aktualizuje wartość value naszego stanu, jednak warunek, który jest postawiony poniżej, jest już prawdziwą loterią, ponieważ nie mamy gwarancji, że kiedykolwiek zostanie on spełniony. Oczywiście przy drugim wywołaniu tej funkcji, możemy się już spodziewać, że będzie, ale jednak oczekujemy, aby spełnił się już podczas pierwszej aktualizacji wartości, ponieważ słowo kluczowe await wskazuje, że oczekujemy na aktualizację stanu. Jednak setState() nie zwraca obiektu Promise, więc await nie ma w tym przypadku absolutnie żadnego znaczenia.

Rozwiązanie problemu

Należy wiedzieć, że setState() nie tylko potrafi przyjąć nowe dane dla naszego stanu, ale również przyjmuje jako callback funkcję, która zostaje wykonana po aktualizacji stanu więc kiedy ona zostanie wykonana, możemy być pewni, że stan został już na pewno zaktualizowany:

class App extends Component {
  state = { value: 0 };

  letsDoIt = () => {
    // this.state.value ma wartość początkową 0
    this.setState({ value: 1 }, () => {
      // warunek będzie spełniony już za pierwszym razem
      if(this.state.value === 1) {
        return 'To był pewniak';
      }
    });
  }
}

Powyższy kod pokazuje, że rozwiązanie problemu nie wymaga wielkiego wysiłku, a po prostu naszym zadaniem jest przekazanie funkcji jako drugi parametr metody setState() oraz umieszczenie w niej kodu, który powinien zostać wykonany dopiero po aktualizacji stanu komponentu.

Rozwiązanie problemu level master

A co jeśli jednak chcemy postawić await przed metodą setState(), ponieważ sprawi to, że kod będzie czytelniejszy? Możemy tutaj wykorzystać jeden z podstawowych mechanizmów programowania obiektowego, a mianowicie dziedziczenie. Wystarczy stworzyć klasę komponentu, która będzie zawierała naszą własną implementację metody setState() oraz zwracała Promise. Każdy inny komponent możemy wówczas dziedziczyć po tej klasie, a przykładowy kod może wyglądać następująco:

class MyComponent extends Component {
  setMyState = (newState) => {
    return new Promise((resolve) => {
      this.setState(newState, resolve);
    });
  }
}

class App extends MyComponent {
  state = { value: 0 };

  letsDoIt = async () => {
    // this.state.value ma wartość początkową 0
    await this.setMyState({ value: 1 });

    // warunek będzie spełniony już za pierwszym razem
    if(this.state.value === 1) {
      return 'To był pewniak';
    }
  }
}

Najważniejsze jest, żeby pamiętać o tym już na samym początku, jeszcze zanim nasza aplikacja React lub React Native sporo urośnie i znalezienie problemu stanie się z różnych powodów mocno utrudnione.