- نُشر في
وداعًا React ، وداعًا useEffect (نأمل)
- الكتاب
- الاسم
- Imamuzzaki Abu Salam
- https://x.com/ImBIOS_Dev
في هذه المقالة، سأوضح لك كيفية استخدام React لاستبدال useEffect في معظم الحالات.
لقد كنت أشاهد "وداعًا، useEffect" بواسطة David Khoursid، وهو 🤯 مذهل بطريقة 😀 جيدة. أوافق على أن useEffect قد تم استخدامه كثيرًا لدرجة أنه يجعل كودنا متسخًا وصعب الصيانة. لقد استخدمت useEffect لفترة طويلة، وأنا مذنب بإساءة استخدامه. أنا متأكد من أن React لديها ميزات ستجعل كودي أنظف وأسهل في الصيانة.
ما هو useEffect؟
useEffect هو عبارة عن هوك يسمح لنا بأداء تأثيرات جانبية في مكونات الدوال. فهو يجمع بين componentDidMount و componentDidUpdate و componentWillUnmount في واجهة برمجة واحدة. إنه هوك جذاب سيمكننا من القيام بأشياء كثيرة. لكنه أيضًا هوك خطير جدًا يمكن أن يسبب الكثير من الأخطاء.
لماذا useEffect خطير؟
دعنا نلقي نظرة على المثال التالي:
import React, { useEffect } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(interval)
}, [])
return <div>{count}</div>
}
إنه عداد بسيط يزداد كل ثانية. يستخدم useEffect لضبط فاصل زمني. كما أنه يستخدم useEffect لمسح الفاصل الزمني عند تفريغ المكون. قطعة الكود أعلاه هي حالة استخدام شائعة لـ useEffect. إنه مثال بسيط، لكنه أيضًا مثال سيئ.
تكمن المشكلة في هذا المثال في أن الفاصل الزمني يتم ضبطه في كل مرة يعاد فيها تقديم المكون. إذا أعيد تقديم المكون لأي سبب من الأسباب، فسيتم ضبط الفاصل الزمني مرة أخرى. سيتم استدعاء الفاصل الزمني مرتين في الثانية. لا تُعد هذه مشكلة في هذا المثال البسيط، لكن يمكن أن تصبح مشكلة كبيرة عندما يكون الفاصل الزمني أكثر تعقيدًا. يمكن أن يسبب أيضًا تسريبات الذاكرة.
كيف يتم إصلاحه؟
هناك العديد من الطرق لإصلاح هذه المشكلة. أحدها هو استخدام useRef لتخزين الفاصل الزمني.
import React, { useEffect, useRef } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
const intervalRef = useRef()
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(intervalRef.current)
}, [])
return <div>{count}</div>
}
الكود أعلاه أفضل بكثير من المثال السابق. لا يحدد الفاصل الزمني في كل مرة يعاد فيها تقديم المكون. لكن لا يزال بحاجة إلى تحسين. لا يزال معقدًا بعض الشيء. ولا يزال يستخدم useEffect، وهو هوك خطير جدًا.
useEffect ليس لتأثيرات
كما نعلم عن useEffect، فهو يجمع بين componentDidMount و componentDidUpdate و componentWillUnmount في واجهة برمجة واحدة. دعنا نقدم بعض الأمثلة على ذلك:
useEffect(() => {
// componentDidMount?
}, [])
useEffect(() => => {
// componentDidUpdate?
}, [something, anotherThing])
useEffect(() => {
return () => {
// componentWillUnmount?
}
}, [])
من السهل فهمه. يتم استخدام useEffect لأداء تأثيرات جانبية عند تثبيت المكون، وتحديثه، وتفريغه. لكن لا يتم استخدامه فقط لأداء تأثيرات جانبية. يتم استخدامه أيضًا لأداء تأثيرات جانبية عند إعادة تقديم المكون. ليس من الجيد أداء تأثيرات جانبية عند إعادة تقديم المكون. يمكن أن يسبب الكثير من الأخطاء. من الأفضل استخدام هوكات أخرى لأداء تأثيرات جانبية عند إعادة تقديم المكون.
import React, { useState, useEffect } from 'react'
const Example = () => {
const [value, setValue] = useState('')
const [count, setCount] = useState(-1)
useEffect(() => {
setCount(count + 1)
})
const onChange = ({ target }) => setValue(target.value)
return (
<div>
<input type="text" value={value} onChange={onChange} />
<div>عدد التغييرات: {count}</div>
</div>
)
}
useEffect ليس مُحدد حالة
import React, { useState, useEffect } from 'react'
const Example = () => {
const [count, setCount] = useState(0)
// مشابه لـ componentDidMount و componentDidUpdate:
useEffect(() => {
// تحديث عنوان المستند باستخدام واجهة برمجة تطبيقات المتصفح
document.title = `لقد نقرت {count} مرة`
}) // <-- هذه هي المشكلة، 😱 يفتقر إلى مصفوفة الاعتماد
return (
<div>
<p>لقد نقرت {count} مرة</p>
<button onClick={() => setCount(count + 1)}>انقر عليّ</button>
</div>
)
}
أوصي بقراءة هذه الوثائق: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
إمبراطوري مقابل إعلاني
إمبراطوري: عندما يحدث شيء ما، قم بتنفيذ هذا التأثير.
إعلاني: عندما يحدث شيء ما، سيؤدي ذلك إلى تغيير الحالة واعتمادًا (مصفوفة التبعية) على أجزاء الحالة التي تغيرت، يجب تنفيذ هذا التأثير، ولكن فقط إذا كان هناك شرط ما صحيحًا. وقد تقوم React بتنفيذه مرة أخرى لأنه ليس هناك سبب للعرض المتزامن.
المفهوم مقابل التنفيذ
المفهوم:
useEffect(() => {
doSomething()
return () => cleanup()
}, [whenThisChanges])
التنفيذ:
useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething()
} else {
doSomethingElse()
}
// أوه، لقد نسيت التنظيف
}, [foo, bar, baz, quo])
تنفيذ العالم الحقيقي:
useEffect(() => {
if (isOpen && component && containerElRef.current) {
if (React.isValidElement(component)) {
ionContext.addOverlay(overlayId, component, containerElRef.current!);
} else {
const element = createElement(component as React.ComponentClass, componentProps);
ionContext.addOverlay(overlayId, element, containerElRef.current!);
}
}
}, [component, containerElRef.current, isOpen, componentProps]);
useEffect(() => {
if (removingValue && !hasValue && cssDisplayFlex) {
setCssDisplayFlex(false)
}
setRemovingValue(false)
}, [removingValue, hasValue, cssDisplayFlex])
من المخيف كتابة هذا الكود. علاوة على ذلك، سيكون أمرًا طبيعيًا في قاعدة كودنا وسيكون فوضويًا. 😱🤮
أين تذهب التأثيرات؟
تعمل React 18 على تنفيذ التأثيرات مرتين عند التثبيت (في الوضع الصارم). التثبيت / التأثير (╯°□°)╯︵ ┻━┻ -> تفريغ (محاكاة) / تنظيف ┬─┬ /( º _ º /) -> إعادة التثبيت / التأثير (╯°□°)╯︵ ┻━┻
هل يجب وضعه خارج المكون؟ useEffect الافتراضي؟ آه ... محرج. همم ... 🤔 لا يمكننا وضعه في العرض لأنه يجب أن لا يكون هناك تأثيرات جانبية هناك لأن العرض يشبه اليد اليمنى لمعادلة رياضية. يجب أن يكون فقط نتيجة الحساب.
ما هو useEffect؟
المزامنة
useEffect(() => {
const sub = createThing(input).subscribe((value) => {
// افعل شيئًا مع value
})
return sub.unsubscribe
}, [input])
useEffect(() => {
const handler = (event) => {
setPointer({ x: event.clientX, y: event.clientY })
}
elRef.current.addEventListener('pointermove', handler)
return () => {
elRef.current.removeEventListener('pointermove', handler)
}
}, [])
تأثيرات العمل مقابل تأثيرات النشاط
إطلاق النار والنسيان مزامنة
(تأثيرات العمل) (تأثيرات النشاط)
0 ---------------------- ----------------- - - -
o o | A | o o | A | A
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o V | V o o V | V |
o-------------------------------------------------------------------------------->
تفريغ إعادة التثبيت
أين تذهب تأثيرات العمل؟
معالجات الأحداث. نوعًا ما.
<form
onSubmit={(event) => {
// 💥 تأثير جانبي!
submitData(event)
}}
>
{/* ... */}
</form>
هناك معلومات ممتازة في Beta React.js. أوصي بقراءتها. خاصة "هل يمكن لمعالجات الأحداث أن يكون لها تأثيرات جانبية؟" جزء.
بالتأكيد! معالجات الأحداث هي أفضل مكان للتأثيرات الجانبية.
مورد رائع آخر أريد ذكره هو حيث يمكنك التسبب في تأثيرات جانبية
في React، عادةً ما تنتمي التأثيرات الجانبية داخل معالجات الأحداث.
إذا كنت قد استنفدت جميع الخيارات الأخرى ولم تتمكن من العثور على معالج الحدث المناسب للتأثير الجانبي الخاص بك، فلا يزال بإمكانك إرفاقه بـ JSX المُرجع مع مكالمة useEffect في مكونك. يُخبر هذا React بتنفيذه لاحقًا، بعد العرض، عندما تُسمح التأثيرات الجانبية. ومع ذلك، يجب أن يكون هذا النهج هو ملجأك الأخير.
"التأثيرات تحدث خارج العرض" - David Khoursid.
(state) => UI
(state, event) => nextState // 🤔 تأثيرات؟
UI هي دالة للحالة. عند عرض جميع الحالات الحالية، ستنتج واجهة UI الحالية. وبالمثل، عندما يحدث حدث، سيتم إنشاء حالة جديدة. وعندما تتغير الحالة، سيتم إنشاء واجهة UI جديدة. هذا النموذج هو جوهر React.
متى تحدث التأثيرات؟
البرامج الوسيطة؟ 🕵️ الاستدعاءات العكسية؟ 🤙 ساجاس؟ 🧙♂️ التفاعلات؟ 🧪 المغسلة؟ 🚰 مونادس؟ 🧙♂️ متى؟ 🤷♂️
انتقالات الحالة. دائمًا.
(state, event) => nextState
|
V
(state, event) => (nextState, effect) // هنا
![صورة توضيحية لإعادة العرض](https://media.slid.es/uploads/174419/images/9663683/CleanShot_2022-06-22_at_20.24.08_2x.png align="left")
أين تذهب تأثيرات العمل؟ معالجات الأحداث. انتقالات الحالة.
الذي يحدث أن يتم تنفيذه في نفس الوقت مع معالجات الأحداث.
قد لا نحتاج إلى تأثير
قد نستخدم useEffect لأننا لا نعرف أن هناك بالفعل واجهة برمجة تطبيقات مدمجة من React يمكن أن تحل هذه المشكلة.
هنا مورد ممتاز للقراءة حول هذا الموضوع: قد لا تحتاج إلى تأثير
لا نحتاج إلى useEffect لـ تحويل البيانات.
useEffect ➡️ useMemo (حتى لو لم نكن بحاجة إلى useMemo في معظم الحالات)
const Cart = () => {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(items.reduce((total, item) => total + item.price, 0))
}, [items])
// ...
}
اقرأها وفكر فيها مرة أخرى بعناية 🧐.
const Cart = () => {
const [items, setItems] = useState([])
const total = useMemo(() => {
return items.reduce((total, item) => total + item.price, 0)
}, [items])
// ...
}
بدلاً من استخدام useEffect
لحساب المجموع، يمكننا استخدام useMemo
لتخزين المجموع في ذاكرة التخزين المؤقت. حتى لو لم يكن المتغير حسابًا مكلفًا، فلن نحتاج إلى استخدام useMemo
لتخزينه في ذاكرة التخزين المؤقت لأننا نُقايض أساسًا الأداء مقابل الذاكرة.
كلما رأينا setState
في useEffect
، فهذه علامة تحذير من أننا يمكن أن نبسطها.
useSyncExternalStore
تأثيرات مع متاجر خارجية؟useEffect ➡️ useSyncExternalStore
❌ طريقة خاطئة:
const Store = () => {
const [isConnected, setIsConnected] = useState(true)
useEffect(() => {
const sub = storeApi.subscribe(({ status }) => {
setIsConnected(status === 'connected')
})
return () => {
sub.unsubscribe()
}
}, [])
// ...
}
✅ أفضل طريقة:
const Store = () => {
const isConnected = useSyncExternalStore(
// 👇 اشترك
storeApi.subscribe,
// 👇 احصل على لقطة
() => storeApi.getStatus() === 'connected',
// 👇 احصل على لقطة الخادم
true
)
// ...
}
لا نحتاج إلى useEffect لـ التواصل مع الوالدين.
useEffect ➡️ eventHandler
❌ طريقة خاطئة:
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
useEffect(() => {
if (isOpen) {
onOpen()
} else {
onClose()
}
}, [isOpen])
return (
<div>
<button
onClick={() => {
setIsOpen(!isOpen)
}}
>
تبديل عرض سريع
</button>
</div>
)
}
📈 طريقة أفضل:
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
const handleToggle = () => {
const nextIsOpen = !isOpen;
setIsOpen(nextIsOpen)
if (nextIsOpen) {
onOpen()
} else {
onClose()
}
}
return (
<div>
<button
onClick={}
>
تبديل عرض سريع
</button>
</div>
)
}
✅ أفضل طريقة هي إنشاء هوك مخصص:
const useToggle({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
const handleToggle = () => {
const nextIsOpen = !isOpen
setIsOpen(nextIsOpen)
if (nextIsOpen) {
onOpen()
} else {
onClose()
}
}
return [isOpen, handleToggle]
}
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, handleToggle] = useToggle({ onOpen, onClose })
return (
<div>
<button
onClick={handleToggle}
>
تبديل عرض سريع
</button>
</div>
)
}
لا نحتاج إلى useEft لـ تهيئة العناصر الفردية العالمية.
useEffect ➡️ justCallIt
❌ طريقة خاطئة:
const Store = () => {
useEffect(() => {
storeApi.authenticate() // 👈 سيتم تشغيل هذا مرتين!
}, [])
// ...
}
🔨 دعنا نصلحه:
const Store = () => {
const didAuthenticateRef = useRef()
useEffect(() => {
if (didAuthenticateRef.current) return
storeApi.authenticate()
didAuthenticateRef.current = true
}, [])
// ...
}
➿ طريقة أخرى:
let didAuthenticate = false
const Store = () => {
useEffect(() => {
if (didAuthenticate) return
storeApi.authenticate()
didAuthenticate = true
}, [])
// ...
}
🤔 كيف إذا:
storeApi.authenticate()
const Store = () => {
// ...
}
🍷 SSR، هاه؟
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
const Store = () => {
// ...
}
🧪 الاختبار؟
const renderApp = () => {
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
appRoot.render(<Store />)
}
لا نحتاج بالضرورة إلى وضع كل شيء داخل مكون.
لا نحتاج إلى useEffect لـ جلب البيانات.
useEffect ➡️ renderAsYouFetch (SSR) أو useSWR (CSR)
❌ طريقة خاطئة:
const Store = () => {
const [items, setItems] = useState([])
useEffect(() => {
let isCanceled = false
getItems().then((data) => {
if (isCanceled) return
setItems(data)
})
return () => {
isCanceled = true
}
})
// ...
}
💽 طريقة Remix:
import { useLoaderData } from '@renix-run/react'
import { json } from '@remix-run/node'
import { getItems } from './storeApi'
export const loader = async () => {
const items = await getItems()
return json(items)
}
const Store = () => {
const items = useLoaderData()
// ...
}
export default Store
⏭️🧹 Next.js (appDir) مع async / await في Server Component way:
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/...')
// قيمة الإرجاع *ليست* مُسلسلة
// يمكنك إرجاع Date و Map و Set وما إلى ذلك.
// توصية: التعامل مع الأخطاء
if (!res.ok) {
// سيتم تنشيط أقرب حدود الخطأ `error.js`
throw new Error('فشل جلب البيانات')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <main></main>
}
⏭️💁 Next.js (appDir) مع useSWR في Client Component way:
// app/page.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>فشل التحميل</div>
if (!data) return <div>جارٍ التحميل...</div>
return <div>أهلاً {data}!</div>
}
⏭️🧹 Next.js (pagesDir) في SSR way:
// pages/index.tsx
import { GetServerSideProps } from 'next'
export const getServerSideProps: GetServerSideProps = async () => {
const res = await fetch('https://api.example.com/...')
const data = await res.json()
return {
props: {
data,
},
}
}
export default function Page({ data }) {
return <div>أهلاً {data}!</div>
}
⏭️💁 Next.js (pagesDir) في CSR way:
// pages/index.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>فشل التحميل</div>
if (!data) return <div>جارٍ التحميل...</div>
return <div>أهلاً {data}!</div>
}
🍃 React Query (SSR way:
import { getItems } from './storeApi'
import { useQuery } from 'react-query'
const Store = () => {
const queryClient = useQueryClient()
return (
<button
onClick={() => {
queryClient.prefetchQuery('items', getItems)
}}
>
عرض العناصر
</button>
)
}
const Items = () => {
const { data, isLoading, isError } = useQuery('items', getItems)
// ...
}
⁉️ حقا ⁉️ ماذا يجب أن نستخدم؟ useEffect؟ useQuery؟ useSWR؟
أو ... فقط use() 🤔
use() هي دالة React جديدة تقبل وعدًا مشابهًا لـ await من الناحية المفاهيمية. تتعامل use() مع الوعد الذي تم إرجاعه بواسطة دالة بطريقة متوافقة مع المكونات والهوكس و Suspense. تعرف على المزيد حول use() في RFC لـ React.
function Note({ id }) {
// هذا يجلب ملاحظة بشكل غير متزامن، ولكن بالنسبة لمؤلف المكون، يبدو
// مثل عملية متزامنة.
const note = use(fetchNote(id))
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
)
}
مشاكل جلب البيانات في useEffect
🏃♂️ ظروف السباق
🔙 لا يوجد زر "العودة" فوري
🔍 لا يوجد محتوى HTML أولي أو SSR
🌊 مطاردة الشلال
- Reddit، Dan Abramov
خاتمة
من جلب البيانات إلى القتال مع واجهات برمجة التطبيقات الإمبراطورية، تعد التأثيرات الجانبية أحد أهم مصادر الإحباط في تطوير تطبيقات الويب. ولنكن صادقين، وضع كل شيء في هوكات useEffect يساعد قليلاً فقط. لحسن الحظ، هناك علم (حسنًا، رياضيات) للتأثيرات الجانبية، تم صياغته في آلات الحالة ورسومات الحالة، والتي يمكن أن تساعدنا في نمذجة التأثيرات وفهم كيفية ترتيبها بصريًا، بغض النظر عن مدى تعقيدها بشكل إعلاني.