State w React.js 2

Ten wpis jest 9 częścią z 16 w kursie React.js

Pod koniec poprzedniego wpisu zadałem podchwytliwe ćwiczenie dotyczące state w React.js. Jeśli jeszcze go nie wykonałaś/eś to teraz jest ten moment, aby wrócić i spróbować 😉 W tym wpisie rozwijam temat state, opisuję dokładniej jak działa setState i jakie argumenty przyjmuje.

Zacznijmy może od wykonania ćwiczenia z poprzedniego wpisu. Zadanie brzmiało tak:

Dodaj dwa nowe liczniki. Pierwszy, który będzie zliczał wszystkie kliknięcia w przyciski (tzn. kliknięcie w + i - daje 0 na obecnym liczniku oraz 2 na nowym liczniku), oraz drugi, który będzie zliczał podwójne kliknięcia (tzw. double click) na elemencie z wynikiem.

Wydaje się proste, ale implementacja odkrywa przez Tobą pewien ważny szczegół dotyczący działania funkcji setState. W jaki sposób chcielibyśmy tutaj aktualizować stan? Musimy przechowywać jeden licznik z sumą, drugi zliczający łączne kliknięcia oraz trzeci, który będzie przechowywał podwójne kliknięcia. To co jest tutaj istotne to fakt, że w momencie pojedynczego kliknięcia aktualizujesz tylko dwa liczniki, a trzeci pozostaje bez zmian. Jak to najprościej zaimplementować?

Jak działa setState?

increment() {
    this.setState({
      sumCount: this.state.counter + 1
      totalCount: this.state.totalCount + 1
      doubleClickCount: this.state.dblClickCount
    })
  }

Działa, po prostu do doubleClickCount zawsze przypisana zostaje niezmieniona wartość this.state.doubleClickCount. Ale czy to konieczne? Co by było, gdyby stan komponentu składał się nie z 3, a z 15 pól? Nie dyskutujmy teraz czy to dobre rozwiązanie, tylko zastanów się jak by musiała wyglądać każda aktualizacja stanu… właśnie.

Na szczęście setState jest mądrzejsze i automatycznie łączy obecny stan z tym podanym mu jako argument — i nadpisuje tylko podane własności. To co się nie zmienia pomijasz:

increment() {
    this.setState({ // doubleClickCount pozostanie niezmienne
      sumCount: this.state.counter + 1
      totalCount: this.state.totalCount + 1
    })
  }

A tutaj w pełni działające rozwiązanie:

See the Pen Stan komponentów React.js by Michał Miszczyszyn (@mmiszy) on CodePen.

Funkcja przekazana do setState

Widzisz, że do setState możemy po prostu przekazać obiekt, który zostanie połączony z obecnym stanem i nadpisze podane własności. I to, do niedawna, była jedyna opcja. Od Reacta 16 polecanym sposobem aktualizowania stanu jest przekazanie do setState funkcji, a nie obiektu. Taka funkcja to tzw. updater. Jak to działa? Updater to taka funkcja, która jako argument przyjmuje obecny stan i jako wynik zwraca obiekt, który zostanie połączony z istniejącym stanem. Przykładowo dla nas:

  increment() {
    this.setState(prevState => {
      return {
        sumCount: prevState.sumCount + 1,
        totalCount: prevState.totalCount + 1
      };
    });
  }

Jakie są zalety tego rozwiązania względem po prostu przekazania obiektu do setState? W tym przypadku żadne. setState jest asynchroniczne (o tym zaraz) i sprawia problemy, gdy wywołamy je kilka razy pod rząd — tutaj pomoże nam updater. Dodatkowo, updater pomaga poprawić wydajność aplikacji — jeśli w updaterze zwrócisz null to nie zostanie wykonany ponowny render. Poświęcę temu wszystkiemu inny wpis!

setState i callback

Dokładnie tak jak w nagłówku. Co to oznacza? Najprościej mówiąc, że wywołanie setState nie zmienia stanu od razu, tylko dopiero po jakimś czasie. Czyli, przykładowo, próba odczytania stanu od razu po jego zmianie przez setState pokaże nam nadal stary, nieaktualny stan. Otwórz konsolę i kliknij w przycisk w tym przykładzie:

See the Pen setState i odczyt state by Michał Miszczyszyn (@mmiszy) on CodePen.

Jak naprawić ten problem? Otóż setState przyjmuje też drugi argument: callback. Jeśli jako drugi argument przekażesz funkcję to zostanie ona wywołana w momencie, gdy stan będzie już zaktualizowany. Spójrz po prostu na przykład (z widoczną konsolą):

See the Pen Callback do setState by Michał Miszczyszyn (@mmiszy) on CodePen.

Jeśli chcesz na bieżąco dowiadywać się o kolejnych częściach kursu React.js to koniecznie śledź mnie na Facebooku i zapisz się na newsletter.

Ćwiczenie

Ćwiczenie: Napisz komponent, który będzie miał dwa inputy na imię i nazwisko. Obok, powinien się wyświetlać tekst wpisany w te pola (imię nazwisko). Użyj do tego atrybutu onInput oraz funkcji setState.

Napisz w komentarzu jak Ci się podoba obsługa formularzy w React. Poświęcę temu jeden z kolejnych odcinków, więc chcę wiedzieć już teraz jakie masz uwagi 🙂

  • Piotr W

    Cześć, kod stworzonego komponentu z ćwiczenia:
    class App extends React.Component {

    constructor(){
    super();
    this.state = {
    name: „”,
    surName: „”
    };
    }

    output(){
    this.setState (() => {
    return {
    name: this.refs.name.value,
    surName: this.refs.surName.value
    }
    })
    }

    render(){
    return (

    {this.state.name} {this.state.surName}

    );
    }
    }

    • Działa 😉 Ale mam uwagę: ref w postaci stringa jest już od pewnego czasu niezalecany.

      Zresztą nie mówiłem jeszcze o nich w tym kursie, więc da się bez refów — do funkcji onInput przekazany jest obiekt event, który zawiera m.in. pole currentTarget — czyli aktualnie edytowany input, z którego można odczytać value.

      • Piotr W

        Super, dzięki za wyjaśnienie. 🙂 Czekam na kolejną część kursu.

  • Bartek

    Czy tak jest dobrze? 🙂

    class App extends React.Component {
    constructor() {
    super();
    this.state = {
    name : „”,
    surName : „”,
    };
    }

    render() {
    return (

    Imie {this.setState({name: e.target.value});} } type=”text”/> {this.state.name}
    Nazwisko {this.setState({surName: e.target.value});}} type=”text”/> {this.state.surName}

    );
    }

    }
    ReactDOM.render(, document.getElementById(„app”));

    • Działa! 🙂 Ale można kilka rzeczy poprawić:

      Do onChange zamiast przekazywać arrow function, lepiej byłoby przekazać metodę klasy — czyli zamiast e => {this.setState(…)} przenieść ten kod do metody np. onInputNameChange. Dlaczego? Czytelność kodu i wydajność.

      Drobne uwagi:
      type="text" jest niepotrzebny.
      <label> powinien mieć w środku inputa albo mieć atrybut for="…".

      I moje czepialstwo: surname, a nie surName 😉

      • Bartek

        Dzięki za code review na pewno wyciągnę wnioski czekam na więcej 😊

  • Michał

    Cześć, moje rozwiązanie:

    class NameForm extends React.Component {
    constructor(props) {
    super(props);
    this.state = {
    first: "",
    last: "",
    };
    this.handleChange = this.handleChange.bind(this);
    }

    handleChange(event) {
    this.setState({[event.target.name]: event.target.value })
    }
    render() {
    return

    First

    Last

    {this.state.first} {this.state.last}
    ;
    }
    }

    • Bardzo ładne rozwiązanie! 🙂
      Szkoda, że Disqus tak psuje JSX — będę musiał pomyśleć co z tym zrobić :/

    • Mat Mic

      Fajne rozwiązanie, właśnie się zastanawiałem jak to zrobić z użyciem tylko jednej funkcji

  • Maciej Grędecki

    Cześć, to jest moje rozwiązanie 😉

    class NameForm extends React.Component {
    constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    }

    handleChange(event) {
    this.setState({value: event.target.value});
    }

    handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
    }

    render() {
    return (

    Name:

    );
    }
    }

    ReactDOM.render(
    ,
    document.getElementById('root')
    );

  • Maciej Grędecki

    Cześć, to jest moje rozwiązanie 😉

    class App extends React.Component{
    constructor(props) {
    super(props);
    this.state = {firstName: '', lastName: ''};
    }
    firstNameChange(event) {
    this.setState({firstName: event.target.value})
    }
    lastNameChange(event) {
    this.setState({lastName: event.target.value})
    }

    render(){
    return(

    {`${this.state.firstName} ${this.state.lastName}`}

    )
    }
    }

    ReactDOM.render(, document.getElementById("app"));