Jaki jest pożytek ze składni funkcjonalnej setState w komponentach funkcyjnych reagowania? [duplikować]
mówimy o posiadaniu elementów funkcjonalnych useState
powiedzmy
const [age, setAge] = useState(0)
teraz powiedzmy, że podczas aktualizacji age
muszę użyć poprzedniejage
Dokumenty React wspominają o czymś, co nazywa się FUNKCJONALNE AKTUALIZACJE, gdzie można przekazać funkcję, a argumentem do niej będzie poprzednia wartość stanu, np.
setState((previousAge) => previousAge + 1)
dlaczego muszę to robić, skoro mogę to zrobić
setState(previousAge + 1)
jakie korzyści płyną z używania funkcjonalności setState
,
Wiem, że w komponentach opartych na klasach było coś, co nazywa się grupowaniem aktualizacji stanu w sposób funkcjonalny, ale nie mogę znaleźć czegoś takiego w dokumentacji komponentów funkcjonalnych.
Odpowiedzi
Nie są takie same, jeśli twoja aktualizacja zależy od poprzedniej wartości znalezionej w stanie, powinieneś użyć formularza funkcjonalnego. Jeśli w tym przypadku nie użyjesz formularza funkcjonalnego, kod czasami się zepsuje.
Dlaczego się psuje i kiedy
Komponenty funkcjonalne React to po prostu zamknięcia, wartość stanu, którą masz w zamknięciu, może być nieaktualna - oznacza to, że wartość wewnątrz zamknięcia nie odpowiada wartości, która jest w stanie React dla tego komponentu, może się to zdarzyć w następujące przypadki:
1- operacje asynchroniczne (w tym przykładzie kliknij powolne dodawanie, a następnie kliknij kilka razy przycisk dodawania, później zobaczysz, że stan został zresetowany do tego, co znajdowało się wewnątrz zamknięcia, gdy kliknięto przycisk powolnego dodawania)
const App = () => {
const [counter, setCounter] = useState(0);
return (
<>
<p>counter {counter} </p>
<button
onClick={() => {
setCounter(counter + 1);
}}
>
immediately add
</button>
<button
onClick={() => {
setTimeout(() => setCounter(counter + 1), 1000);
}}
>
Add
</button>
</>
);
};
2- W przypadku wielokrotnego wywoływania funkcji aktualizacji w tym samym zamknięciu
const App = () => {
const [counter, setCounter] = useState(0);
return (
<>
<p>counter {counter} </p>
<button
onClick={() => {
setCounter(counter + 1);
setCounter(counter + 1);
}}
>
Add twice
</button>
</>
);
}
Mogą wystąpić problemy w zależności od tego, jak szybko / często wywoływany jest seter.
Jeśli używasz prostego sposobu, pobierając wartość z zamknięcia, kolejne wywołania między dwoma renderami mogą nie przynieść pożądanego efektu.
Prosty przykład:
function App() {
const [counter, setCounter] = useState(0);
const incWithClosure = () => {
setCounter(counter + 1);
};
const incWithUpdate = () => {
setCounter(oldCounter => oldCounter + 1);
};
return (<>
<button onClick={_ => { incWithClosure(); incWithClosure(); }}>
Increment twice using incWithClosure
</button>
<button onClick={_ => { incWithUpdate(); incWithUpdate(); }}>
Increment twice using incWithUpdate
</button>
<p>{counter}</p>
</>);
}
Oba przyciski dwukrotnie wywołują jedną z metod inkrementacji. Ale obserwujemy:
- Pierwszy przycisk zwiększy licznik tylko o 1
- Drugi przycisk zwiększy licznik o 2, co jest prawdopodobnie pożądanym wynikiem.
Kiedy to się może stać?
- Oczywiście, jeśli
incWithClosure
jest wywoływana wiele razy bezpośrednio po sobie - Jeśli zaangażowane są zadania asynchroniczne, może się to łatwo zdarzyć (patrz poniżej)
- Być może, jeśli React ma dużo do zrobienia, jego algorytmy planowania mogą zdecydować o obsłudze wielu bardzo szybkich kliknięć za pomocą tego samego modułu obsługi zdarzeń
Przykład z pracą asynchroniczną (symulacja ładowania zasobu):
function App() {
const [counter, setCounter] = useState(0);
const incWithClosureDelayed = () => {
setTimeout(() => {
setCounter(counter + 1);
}, 1000);
};
const incWithUpdateDelayed = () => {
setTimeout(() => {
setCounter((oldCounter) => oldCounter + 1);
}, 1000);
};
return (
<>
<button onClick={(_) => incWithClosureDelayed()}>
Increment slowly using incWithClosure
</button>
<button onClick={(_) => incWithUpdateDelayed()}>
Increment slowly using incWithUpdate
</button>
<p>{counter}</p>
</>
);
}
Kliknij dwukrotnie pierwszy przycisk (w ciągu jednej sekundy) i obserwuj, że licznik jest zwiększany tylko o 1. Drugi przycisk zachowuje się poprawnie.
Ponieważ jeśli tego nie zrobisz , w pewnym momencie okaże się, że otrzymasz starą wartość age
. Problem w tym, że czasami to, co sugerujesz, zadziała. Ale czasami tak nie jest. Może nie zepsuć się w obecnym kodzie dzisiaj, ale może się zepsuć w innym kodzie napisanym kilka tygodni temu lub w obecnym kodzie za kilka miesięcy.
Objaw jest naprawdę, naprawdę dziwny. Możesz wydrukować wartość zmiennej wewnątrz komponentu jsx, używając {x}
składni, a później wydrukować tę samą zmienną, używając console.log
po wyrenderowaniu komponentu jsx (nie wcześniej) i stwierdzić, że console.log
wartość jest nieaktualna - to, console.log
co dzieje się po renderowaniu, może w jakiś sposób mieć starszą wartość niż renderowanie.
Zatem rzeczywista wartość zmiennych stanu może nie zawsze działać poprawnie w zwykłym kodzie - są one przeznaczone tylko do zwracania najnowszej wartości w renderowaniu. Z tego powodu zaimplementowano mechanizm wywołania zwrotnego w programie ustawiającym stan, aby umożliwić uzyskanie najnowszej wartości zmiennej stanu w zwykłym kodzie poza renderowaniem.