ReactJS Jak dokładnie działa useState w moim kodzie?

1

Mam problem ze zrozumieniem useState w poniższym kodzie. Kiedy przykładowo const [x, setX] = useState([]), to przy pierwszym renderze zmienna x jest pustą tablicą, ale po zmianie wartości za pomocą setX, przy kolejnym renderze komponentu, useState([]) - argument (czyli pusta tablica) jest ignorowany i zastępowany nową wartością. Tak jak w dokumentacji: https://pl.reactjs.org/docs/hooks-rules.html#explanation

Ale co w sytuacji, gdy argumentem jest funkcja, która jest od razu wywoływana i zapisuje klucz z wartością do localStorage, tak jak poniżej.
Przy pierwszym renderze zwracana jest pusta tablica (ponieważ zmienna list = null). Po kliknięciu Submit i wywołaniu setIgredients oraz ponownym wyrenderowaniu komponentu, getLocalStorage() nie jest pominięty, tylko jeszcze raz się wywołuje. Czyli ostatecznie co jest podmieniane przy kolejnym renderze dla igredients: wartość zwrócona z getLocalStorage() czy [...igredients, newProduct]?

Snippet: https://codesandbox.io/embed/quizzical-saha-m6900?fontsize=14&hidenavigation=1&theme=dark

import React, { useEffect, useState } from "react";

const getLocalStorage = () => {
  let list = localStorage.getItem("list");
  // console.log(list)
  if (list) {
    console.log(1);
    return JSON.parse(localStorage.getItem("list"));
  } else {
    return [];
  }
};

const GroceryBud = () => {
  const [product, setProduct] = useState("");
  const [igredients, setIgredients] = useState(getLocalStorage());  // <---
  const [error, setError] = useState("");
  const [success, setSuccess] = useState("");
  const [btnType, setBtnType] = useState("Submit");

  const handleChange = (e) => {
    setProduct(e.target.value);
  };

  const messageHandler = (message, text) => {
    message(text);
    setTimeout(() => {
      message("");
    }, 2500);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (product) {
      if (btnType === "Submit") {
        const newProduct = { id: new Date().getTime(), product };
        setIgredients([...igredients, newProduct]);
        setProduct("");
        messageHandler(setSuccess, "Item Added To The List");
      }
    } else {
      messageHandler(setError, "Please Enter Value");
    }
  };

  useEffect(() => {
    localStorage.setItem("list", JSON.stringify(igredients));
  }, [igredients]);

  return (
    <div id="wrapper">
      {error && <p className="warning">{error}</p>}
      {success && <p className="success">{success}</p>}
      <h2>Grocery Bud</h2>
      <form>
        <input
          type="text"
          className="product-field"
          placeholder="e.g. eggs"
          value={product}
          onChange={handleChange}
        />
        <button type="submit" className="submit" onClick={handleSubmit}>
          {btnType}
        </button>
      </form>
      <div className="igredients">
        {igredients.map((item, index) => {
          const { id, product } = item;
          return (
            <article key={id}>
              <p>{product}</p>
            </article>
          );
        })}
      </div>
    </div>
  );
};
3

Po kliknięciu Submit i wywołaniu setIgredients oraz ponownym wyrenderowaniu komponentu, getLocalStorage() nie jest pominięty, tylko jeszcze raz się wywołuje. C

"it's just JavaScript". To, że twórcy Reacta robią magię i próbują zrobić wrażenie, że React jest osobnym językiem programowania, nie znaczy, że JSa nagle zacznie się zachowywać pod dyktando Reacta.

Jeśli funkcja renderująca się wywołuje podczas każdego rendera, to oczywiste jest, że getLocalStorage się też wywoła za każdym razem (co najwyżej React zignoruje tę wartość).

Takie rzeczy jak zapis/odczyt z localStorage nie powinien być luzem w funkcji renderującej (tak jak teraz masz), tylko np. w hooku useEffect.

setIgredients

literówka. Ingredients

3

Jeżeli chcesz wywołać funkcję inicjalizującą tylko raz, to zamiast podawać wynik funkcji:

const [igredients, setIgredients] = useState(getLocalStorage()); 

podaj funkcję jako argument:

const [igredients, setIgredients] = useState(() => getLocalStorage()); 

albo nawet tak:

const [igredients, setIgredients] = useState(getLocalStorage); 

https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily

0

@LukeJL:

LukeJL napisał(a):

Jeśli funkcja renderująca się wywołuje podczas każdego rendera, to oczywiste jest, że getLocalStorage się też wywoła za każdym razem (co najwyżej React zignoruje tę wartość).

Nie wiem, czy na pewno tak jest, ale wartość zwracana z getLocalStorage jest nadpisywana przez to co podałem jako argument w setIngredients.
Czyli useState(getLocalStorage()) --> najpierw getLocalStorage(), od razu po tym podmieniane jest na [...ingredients, newProduct].

2
user656 napisał(a):

Nie wiem, czy na pewno tak jest, ale wartość zwracana z getLocalStorage jest nadpisywana przez to co podałem jako argument w setIngredients.

Skąd taki pomysł?

W kodzie z pierwszego postu, metoda getLocalStorage jest wywoływana przy każdym renderze, ale React korzysta ze zwróconej wartości tylko przy pierwszym renderze. Wszystkie kolejne wywołania getLocalStorage to niepotrzebne przepalanie procesora, bo wyniki i tak są ignorowane.

Potestuj sobie takie coś:

const randomizeX = () => {
  const r = Math.random()
  console.log(r);
  return r;
}

const App = () => {

  const [x, setX] = useState(randomizeX());

  const onClick = () => setX(p => p + 10);

  return (
    <div className="App">
      <h1>{x}</h1>
      <button onClick={onClick}>Add 10</button>
    </div>
  );
}

export default App;

Zobaczysz że metoda randomizeX jest wywoływana przy każdym renderze, ale kolejne jej wyniki nie mają już wpływu na stan.
Skoro kolejne wywołania randomizeX nie mają znaczenia to lepiej użyć np.:

const [x, setX] = useState(() => randomizeX());

1 użytkowników online, w tym zalogowanych: 0, gości: 1