W dzisiejszym artykule przybliżę temat związany z initial state, na podstawie otrzymanych props. Bardzo często takie działanie jest nazywane antywzorcem, ponieważ może powodować to problemy z naszym komponentem…

O co w tym wszystkim chodzi i dlaczego należy uważać na takie rozwiązanie?
Przyjrzyjmy się poniższemu fragmentowi kodu:

constructor(props){
  super(props)
  this.state = {
    inputValue: props.inputValue
  }
}

State komponentu definiowane jest na podstawie props i potencjalnie nie ma tutaj nic dziwnego.

Spójrzmy teraz na inny przykład.
Będziemy mieli komponent główny <ColorMaker>, w którym definiowany jest kolor. Kolor możemy zmieniać klikając na przycisk. Na podstawie koloru renderowany jest inny komponent <ColorComponent/> z tłem w podanym kolorze…

class ColorComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            color: props.color
        }
    }

    render() {
        return (
            <div style={{'backgroundColor': this.state.color}}>Color background</div>
        )
    }
}

function ColorMaker() {
  const getColor = () => (
      ['blue', 'yellow', 'red', 'green'][Math.floor(Math.random() * 4)]
  )

  const [color, setColor] = React.useState(getColor());

  return (
      <div>
          <button onClick={() => setColor(getColor())}>Change color</button>
          <ColorComponent color={color} />
          Color from state: {color}
      </div>
  );
}

Komponent <ColorMaker /> podczas inicjowania state o nazwie color otrzymuje losowy kolor dzięki metodzie getColor(), następnie kliknięcie przycisku, zmienia dany kolor, co widać poprzez wyświetlenie koloru w sekcji Color from state: …
Kolor ten jest przekazywany do komponentu <ColorComponent />.

Teraz, podczas pierwszej inicjalizacji komponent <ColorComponent /> renderuje się poprawnie używając wygenerowanego losowo koloru. Po kliknięciu na przycisk i kolejnej zmianie koloru, komponent jednak nie zmienia tła.

Dlaczego tak się dzieje?
Dzieje się tak dlatego, że komponent <ColorComponent/> został już zamontowany i konstruktor wykonał się tylko raz (celowo użyłem komponentu klasowego), a co za tym idzie stan również zainicjował się jednorazowo. Teraz nawet, gdy otrzymamy nową wartość poprzez props, nasz komponent <ColorComponent/> nie zareaguje na zmianę. Oczywiście naprawa jest bardzo prosta. Wystarczy korzystać z props bezpośrednio w metodzie render().

Przykład był bardzo prosty. Miał jednak pokazać, jak działa definiowanie stanu na podstawie props oraz jak może wpłynąć to na zachowanie naszego komponentu.

Mała uwaga…. To samo zadzieje się, jeżeli nie będziemy mieć zdefiniowanego construct(), a state będziemy inicjować w ten sposób. Jest to analogiczny w działaniu zapis:

class ColorComponent extends React.Component {
    state = {
        color: this.props.color
    }
...

Poniżej jeszcze <ColorComponent/> napisany funkcyjnie:

function ColorComponent(props) {
    const [color] = React.useState(props.color);

    return (
        <div style={{'backgroundColor': color}}>Color background</div>
    )
}

Co na to React?

Sam React, na oficjalnej stronie https://reactjs.org/docs/react-component.html#constructor w części
Avoid copying props into state! This is a common mistake
mówi o tym, o czym piszemy w artykule.
Tylko w bardzo specyficznych przypadkach, kiedy świadomie nie chcemy odświeżać stanu komponentu i korzystać z zainicjowanej wartości, możemy pokusić się o takie rozwiązanie. Oczywiście komponent też może zawierać własną metodę, która ten stan później zmienia. W ten sposób mamy możliwość zmiany zainicjowanego wcześniej state.

Oczywiście możemy próbować znaleźć przypadki użycia dla inicjowania stanu bezpośrednio na podstawie props. Przeszukując internet można na nieliczne przypadki natrafić. Więcej jednak jest artykułów, które jasno mówią, że jest to antywzorzec. Uważam też, że nie bez przyczyny na oficjalnej stronie React pojawiła się o tym wzmianka…

Co w zamian?
Według mnie lepiej byłoby, aby nasz komponent reagował na zmianę props, a za te „propsy” odpowiadał komponent główny, który tymi zmianami by sterował… Coś na zasadzie <Rodzic>, który steruje komponentami <Dzieci>.
W tym miejscu, jeżeli komponent <Rodzic> zmienia się zbyt często i nasze komponenty <Dzieci> również ulegają nadmiernym „przeładowaniom”, można pomyśleć o optymalizacji lub innej organizacji kodu.

Dlaczego nie warto?

Pomyślmy też o tym „rozwiązaniu” z innej strony…
Mamy dosyć złożony komponent z daną logiką. Z komponentu głównego przekazujemy do komponentu podrzędnego jakieś props, komponent podrzędny inicjuje state na podstawie props. Na ten moment wszystko działa jak należy, komponent podrzędny renderuje się poprawnie.
Po kilku tygodniach inny programista musi dodać kawałek logiki w komponencie głównym. Logika ta zmienia kilka „propsów”, które trafiają do komponentu niżej. Dosyć naturalne jest, że w tej sytuacji liczymy, że komponent podrzędny się odświeży używając zmienionych wartości w props. Tak przecież działa 99,9% Reactowych komponentów… W tym przypadku tak się niestety nie stanie i zapewne w tym miejscu zacznie się debugowanie kodu…

Mam nadzieję, że po przeczytaniu artykułu bardzo świadomie podejdziesz do inicjowania stanu na podstawie props. Jeżeli jednak takie coś będzie miało miejsce, będzie to celowe działanie poparte konkretnymi argumentami z Twojej strony.

Dzięki!

Źródła:
Obraz: https://unsplash.com/photos/YEWvMidcKkg
React constructor: https://reactjs.org/docs/react-component.html#constructor

Łukasz
Autor

Full-Stack Developer (React/PHP/MySQL). Przygodę z technologiami web rozpocząłem jeszcze jako nastolatek-pasjonat. Przez lata zdobywałem doświadczenie jako freelancer. Od ponad 7 lat pracuję dla dużych firm polskich i międzynarodowych, uczestnicząc w kompleksowych projektach IT. Liczę, że wiedza i przemyślenia, którymi dzielę się na tym blogu, będą Ci pomocne.

Napisz komentarz

Zwracam szczególną uwagę na kulturę komentarzy i nie będę akceptował żadnego hejtu. Jeżeli masz ochotę coś skrytykować, proszę bardzo - konstruktywna krytyka jest wskazana.

× 7 = 35