javascript - Bileşende useState'den gelen state güncelleyiciye yapılan birden fazla çağrı, birden fazla yeniden render'a neden olur
React kancalarını (hooks) ilk kez deniyorum ve her şey iyi görünüyordu ta ki veri aldığımda ve iki farklı durum değişkenini (data ve loading flag) güncellediğimde, bileşenim (bir veri tablosu) iki kez render edildiğini fark edene kadar, oysa her iki durum güncelleme çağrısı aynı fonksiyonda gerçekleşiyor. İşte her iki değişkeni bileşenime döndüren api fonksiyonum.
const getData = url => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setLoading(false);
setData(test.data.results);
}
}, []);
return { data, loading };
};
Normal bir sınıf bileşeninde genellikle durumu güncellemek için tek bir çağrı yaparsınız, bu durum karmaşık bir nesne olabilir, ancak "hooks yöntemi" durumun daha küçük birimlere bölünmesi gibi görünüyor, bunun bir yan etkisi olarak ayrı ayrı güncellendiklerinde birden fazla yeniden render oluşuyor gibi görünüyor. Bunu azaltmanın bir yolu var mı?
Cevap #1
loading durumunu ve data durumunu tek bir durum nesnesinde birleştirebilirsiniz ve sonra tek bir setState çağrısı yapabilirsiniz, böylece sadece bir render gerçekleşir.
Not: Sınıf bileşenlerindeki setState'in aksine, useState'ten dönen setState nesneleri mevcut durumla birleştirmez, nesneyi tamamen değiştirir. Eğer birleştirme yapmak istiyorsanız, önceki durumu okuyup onu yeni değerlerle kendiniz birleştirmeniz gerekir. Belgeleri inceleyin.
Performans sorununuz olduğunu belirlemeden aşırı şekilde render çağırmak konusunda fazla endişelenmezdim. React bağlamında render yapmak ve sanal DOM güncellemelerini gerçek DOM'a uygulamak farklı konulardır. Buradaki render, tarayıcı DOM'unu güncellemek ile ilgili değil, sanal DOM'ları üretmekle ilgilidir. React, setState çağrılarını toplu işleyebilir ve tarayıcı DOM'unu son yeni durumla güncelleyebilir.
const {useState, useEffect} = React;
function App() {
const [userRequest, setUserRequest] = useState({
loading: false,
user: null,
});
useEffect(() => {
// Note that this replaces the entire object and deletes user key!
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));<script src="https://unpkg.com/[email /umd/react.development.js"></script>
<script src="https://unpkg.com/[email /umd/react-dom.development.js"></script>
<div id="app"></div>Alternatif - kendi durum birleştirme kancanızı yazın
const {useState, useEffect} = React;
function useMergeState(initialState) {
const [state, setState] = useState(initialState);
const setMergedState = newState =>
setState(prevState => Object.assign({}, prevState, newState)
);
return [state, setMergedState];
}
function App() {
const [userRequest, setUserRequest] = useMergeState({
loading: false,
user: null,
});
useEffect(() => {
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));<script src="https://unpkg.com/[email /umd/react.development.js"></script>
<script src="https://unpkg.com/[email /umd/react-dom.development.js"></script>
<div id="app"></div>Cevap #2
Bunun ayrıca useReducer kullanarak başka bir çözümü de var! Önce yeni setState'imizi tanımlıyoruz.
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
bunun ardından onu iyi eski sınıflar this.setState gibi basitçe kullanabiliriz, sadece this olmadan!
setState({loading: false, data: test.data.results})
Yeni setState’imizde fark etmiş olabileceğiniz gibi (tıpkı daha önce this.setState ile yaptığımız gibi), tüm durumları birlikte güncellememize gerek yok! Örneğin, durumlarımızdan birini şu şekilde değiştirebilirim (ve bu diğer durumları etkilemez!):
setState({loading: false})
Harika, ha?!
O halde tüm parçaları bir araya koyalım:
import {useReducer} from 'react'
const getData = url => {
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null}
)
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setState({loading: false, data: test.data.results})
}
}, [])
return state
}
Typescript Desteği. Bu çözümü yanıtlayan P. Galbraith'e teşekkürler, Typescript kullananlar şunu kullanabilir:
useReducer<Reducer<MyState, Partial<MyState>>>(...)
burada MyState, durum nesnenizin türüdür.
örneğin. Bizim durumumuzda bu şekilde olacak:
interface MyState {
loading: boolean;
data: any;
something: string;
}
const [state, setState] = useReducer<Reducer<MyState, Partial<MyState>>>(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
Önceki Durum Desteği. Yorumlarda user2420374, setState içinde prevState'e erişim sağlamanın bir yolunu sordu, işte bu hedefi gerçekleştirmek için bir yol:
const [state, setState] = useReducer(
(state, newState) => {
newWithPrevState = isFunction(newState) ? newState(state) : newState
return (
{...state, ...newWithPrevState}
)
},
initialState
)
// And then use it like this...
setState(prevState => {...})
isFunction, geçirilen argümanın bir fonksiyon (bu, prevState'e erişmeye çalıştığınız anlamına gelir) mi yoksa düz bir nesne mi olduğunu kontrol eder. Alex Grande tarafından yapılan bu isFunction uygulamasını burada bulabilirsiniz.
Duyuru. Bu cevabı çok kullanmak isteyenler için, bunu bir kütüphaneye dönüştürmeye karar verdim. Buradan bulabilirsiniz:
Github: https://github.com/thevahidal/react-use-setstate
NPM: https://www.npmjs.com/package/react-use-setstate
Cevap #3
React-hooks'ta güncellemeleri batchelmek https://github.com/facebook/react/issues/14259
React şu anda, bir buton tıklaması veya giriş değişikliği gibi React tabanlı bir etkinlikten tetiklenirse durum güncellemelerini toplu olarak işler. Bir asenkron çağrı gibi bir React etkinlik işleyicisinin dışında tetiklenirse güncellemeleri toplu olarak işlemez.
Cevap #4
Bu işe yarar:
const [state, setState] = useState({ username: '', password: ''});
// later
setState({
...state,
username: 'John'
});
Cevap #5
Sınıf bileşenlerinden this.setState birleştirme davranışını çoğaltmak için, React belgeleri, nesne yayılımı ile useState fonksiyonel biçimini kullanmayı önerir - useReducer kullanımına gerek yoktur:
setState(prevState => {
return {...prevState, loading, data};
});
İki durum şimdi tek bir durumda birleştirildi, bu da size bir render döngüsü kazandıracak.
Tek bir durum nesnesiyle başka bir avantaj daha var: loading ve data bağımlı durumlardır. Durum birlikte konulduğunda, geçersiz durum değişiklikleri daha belirgin hale gelir:
setState({ loading: true, data }); // ups... loading, but we already set data
Durumları tutarlı hale getirmeyi daha da iyi sağlayabilirsiniz: 1.) durumunuzu loading, success, error vb. olarak açık bir şekilde belirleyerek ve 2.) durum mantığını bir azaltıcıda kapsüllemek için useReducer kullanarak:
const useData = () => {
const [state, dispatch] = useReducer(reducer, /*...*/);
useEffect(() => {
api.get('/people').then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// keep state consistent, e.g. reset data, if loading
else if (status === "loading") return { ...state, data: undefined, status };
return state;
};
const App = () => {
const { data, status } = useData();
return status === "loading" ? <div> Loading... </div> : (
// success, display data
)
}
const useData = () => {
const [state, dispatch] = useReducer(reducer, {
data: undefined,
status: "loading"
});
useEffect(() => {
fetchData_fakeApi().then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
return state;
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// e.g. make sure to reset data, when loading.
else if (status === "loading") return { ...state, data: undefined, status };
else return state;
};
const App = () => {
const { data, status } = useData();
const count = useRenderCount();
const countStr = `Re-rendered ${count.current} times`;
return status === "loading" ? (
<div> Loading (3 sec)... {countStr} </div>
) : (
<div>
Finished. Data: {JSON.stringify(data)}, {countStr}
</div>
);
}
//
// helpers
//
const useRenderCount = () => {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return renderCount;
};
const fetchData_fakeApi = () =>
new Promise(resolve =>
setTimeout(() => resolve({ ok: true, data: { results: [1, 2, 3] } }), 3000)
);
ReactDOM.render(<App />, document.getElementById("root"));<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>Not: Özel Hook'ların use ile başlamasına dikkat edin (getData yerine useData). Ayrıca useEffect'e geçirilen callback async olamaz.
