poniedziałek, 19 października 2020

react hooks - useState

Hooki w react pojawiły się pod koniec 2018 roku. Umożliwjają one wygodne pisanie komponentów funkcyjnych, dzielenie odpowiedzialności w kodzie oraz tworzenie małych plików - łatwych w testowaniu, utrzymaniu i wygodnych, gdy trzeba znaleźć błąd.

Najbardziej podstawowym hookiem jest useState(). Umożliwia on zarządzanie wartością stanu pomiędzy kolejnymi wywołaniami funkcji komponentu.

useState() zwraca tablicę z dwiema wartościami: stanem i funkcją do jego aktualizacji. Zgodnie z konwencją nazwa funkcji do aktualizacji powinna zaczynać się od czasownika set, czyli ustawiać. Jeśli zmienną stanu nazwiemy user to funcja powinna nazywać się setUser.
const [state, setState] = useState();

useState() przyjmuje jako parametr początkową wartość stanu
const [state, setState] = useState(4);
console.log(state); // 4

Funkcja setState przyjmuje jako parametr nową wartość stanu lub callback
const [state, setState] = useState(4);
console.log(state); // 4
setState(24);
console.log(state); // 24

Callback przyjmuje jako parametr aktualna wartość lub referencję stanu:
const [state, setState] = useState(4);
console.log(state); // 4
setState(state => state + 1);
console.log(state); // 5

Po co setState przyjmuje callback jako parametr? Po to, żeby w każdym przypadku, gdy nasz kod nie ma dostępu do aktualnego state móc jednak z niego korzystać. Dość toporny przykład pokazujący jak to działa jest z wykorzystaniem setInterval
.
import React from "react";

export default function App() {
  const [state, setState] = React.useState(0);

  function updateState() {
    setState((state || 0) + 1);
  }

  React.useEffect(() => {
    const interval = setInterval(updateState, 3000);
    return () => clearInterval(interval);
  }, []);

  return (
    <div className="App">
      {console.log("rerender", state)}
      <h3>{state}</h3>
    </div>
  );
}

Powyższy przykład pokazuje ustawianie nowej wartości stanu co 3 sekundy. Niestety, stan ustawia się na 1, cały czas co 3 sekundy, ponieważ funkcja updateState jest utworzona, gdy state wynosi 0. I tak zostaje przekazana do setInterval. Co 3 sekundy setInterval zwieksza state równy 0 o 1...
zobacz na Sandbox

Sposobem na naprawienie (poza zabawą scope i clojure) jest użycie callbacku jako parametru w setState, wtedy cała funkcja bedzie wygladać tak:
zobacz na Sandbox

  function updateState() {
    setState(state => (state || 0) + 1);
  }

state przekazywany do callbacku jest tym aktualnym, wiec co 3 sekundy rośnie jego wartośc o 1.

react i referencja

Cykl życia komponentu reacta obejmuje update komponentu, gdy dane na których pracuje komp[onent się zmienią. Dotyczy to zarówno danych przychodzacych z zewnątrz (props) jak i tych tworzonych i uaktualnianych wewnątrz komponentu (state). Jeśli chodzi o dane prymitywne (liczby, ciągi tekstowe, typy logiczne, null, undefined i Symbol) to sprawa jest dość prosta. Wiadomo, że:
console.log(1 === 1); // true
console.log('a' === 'a'); // true
console.log(true === true); // true
console.log(undefined === undefined); // true
console.log(Symbol('a').toString() === Symbol('a').toString()); // true

Czyli jeśli wartość props lub state zmieni się z 1 na 2 to komponent wie, że ma się zrerenderować. Proste.

A jednak, nie do końca. Wszystko co nie jest prymitywem w JS (jest to pewne uproszczenie, będzie osobny wpis na ten temat) jest obiektem. A obiekty nie są przekazywane przez wartość a przez referencję. Innymi słowy JS umieszcza utworzony obiekt gdzieś w pamięci i posługuje się adresem tego obiektu a nie nim samym. Każde utworzenie obiektu tworzy nową referencję.
const a = { id: 1 };
const b = { id: 1 };

console.log(a === a); // true
console.log(a === b); // false
console.log(Object.is(a, a)); // true console.log(Object.is(a, b)); // false

Czyli nawet dwa identyczne obiekty mają dwie różne referencje, więc nie da się ich łatwo porównać. React do porównywania obiektów używa Object.is().

Dlatego, gdy zmieniamy jakąś wartość w obiekcie, powinniśmy stworzyć nową referencję, żeby react zauważył zmianę, na przykład tak:
const [formData, setFormData] = useState({});

function handleChange(evt) {
  const id = evt.target.id;
  const value = evt.target.value;

  const formCopy = { ...formData };
  formCopy[id] = value;
  setFormData(formCopy);
}

Alternatywnym sposobem może być używanie biblioteki jak immutable.js aby każda zmiana w obiekcie zwracała nową referencję.

sobota, 30 maja 2020

Function vs arrow function

Arrow function came in 2015 together with new ECMAScript 5. It was a revolution! No more var that = this; to be able to use parent context in a callback.

I really enjoyed this time and like others was using arrow functions everywhere. But 5 years later I started to hate arrow functions used as normal functions.

Code with one-liner functions and other variables:
const getData = dataKey => get(state, dataKey);
const makePlainObj = () => ({});
const doSth = ({ a, b }) => {console.log(b)};
const abc = { get: someFn };


Code with normal functions:
function getData(dataKey) {
  return get(state, dataKey);
}

function makePlainObj() {
  return {};
}

function doSth({ a, b }) {
  console.log(b);
}

const abc = { get: someFn };

Yes, second code is longer but at the same time much more readable. On first look it is visible what is a variable and what is a function. Also functions are hoisted when created as variables arrow functions are not.