يوم جيد ، أيها الأصدقاء!
أقدم انتباهكم إلى أفضل عشرة خطاطيف مخصصة .
جدول المحتويات
- useMemoCompare
- useAsync
- useRequireAuth
- useRouter
- useAuth
- useEventListener
- useWhyDidYouUpdate
- useDarkMode
- useMedia
- useLocalStorage
useMemoCompare
يشبه هذا الخطاف useMemo ، ولكن بدلاً من مصفوفة التبعيات ، يتم تمرير وظيفة تقارن القيم السابقة والقيم الجديدة. يمكن للدالة مقارنة الخصائص المتداخلة أو استدعاء طرق على الكائنات أو القيام بشيء آخر لأغراض المقارنة. إذا أعادت الدالة صحيحًا ، فسيقوم الخطاف بإرجاع مرجع للكائن القديم. وتجدر الإشارة إلى أن هذا الخطاف ، على عكس useMemo ، لا يعني عدم وجود حسابات معقدة متكررة. يحتاج إلى تمرير القيمة المحسوبة للمقارنة. يمكن أن يكون هذا مفيدًا عندما تريد مشاركة المكتبة مع مطورين آخرين ولا تريد إجبارهم على تذكر الكائن قبل الإرسال. إذا تم إنشاء كائن في جسم مكون (في الحالة التي يعتمد فيها على الخاصيات) ، فسيكون جديدًا في كل تصيير. إذا كان الكائن تبعية لـ useEffect ، فسيتم تفعيل التأثير في كل تصيير ،والتي يمكن أن تؤدي إلى مشاكل تصل إلى حلقة لا نهاية لها. يسمح لك هذا الخطاف بتجنب تطور الأحداث هذا باستخدام مرجع الكائن القديم بدلاً من المرجع الجديد إذا تعرفت الوظيفة على الكائنات على أنها نفسها.
import React, { useState, useEffect, useRef } from "react";
//
function MyComponent({ obj }) {
const [state, setState] = useState();
// , "id"
const objFinal = useMemoCompare(obj, (prev, next) => {
return prev && prev.id === next.id;
});
// objFinal
// obj , , obj
// ,
// , ,
// -> -> -> ..
useEffect(() => {
//
return objFinal.someMethod().then((value) => setState(value));
}, [objFinal]);
// [obj.id] ?
useEffect(() => {
// eslint-plugin-hooks , obj
// eslint-disable-next-line
//
return obj.someMethod().then((value) => setState(value));
}, [obj.id]);
}
//
function useMemoCompare(next, compare) {
// ref
const prevRef = useRef();
const prev = prevRef.current;
//
//
const isEqual = compare(prev, next);
// , prevRef
//
// , true,
useEffect(() => {
if (!isEqual) {
prevRef.current = next;
}
});
// ,
return isEqual ? prev : next;
}
useAsync
من الممارسات الجيدة عرض حالة الطلب غير المتزامن. مثال على
ذلك هو جلب البيانات من واجهة برمجة التطبيقات وعرض مؤشر التحميل قبل تقديم النتائج. مثال آخر هو تعطيل زر أثناء تقديم النموذج ثم عرض النتيجة. بدلاً من تلويث المكون بالكثير من استدعاءات useState لتتبع حالة الوظيفة غير المتزامنة ، يمكننا استخدام هذا الخطاف ، الذي يأخذ وظيفة غير متزامنة ويعيد القيمة والخطأ والحالة حسب الحاجة لتحديث واجهة المستخدم. القيم المحتملة لخاصية "الحالة" هي "خامل" و "معلق" و "نجاح" و "خطأ". يسمح لك الخطاف الخاص بنا بتنفيذ وظيفة على الفور أو في وقت متأخر باستخدام وظيفة التنفيذ.
import React, { useState, useEffect, useCallback } from 'react'
//
function App() {
const {execute, status, value, error } = useAsync(myFunction, false)
return (
<div>
{status === 'idle' && <div> </div>}
{status === 'success' && <div>{value}</div>}
{status === 'error' && <div>{error}</div>}
<button onClick={execute} disabled={status === 'pending'}>
{status !== 'pending' ? ' ' : '...'}
</button>
</div>
)
}
//
// 50%
const myFunction = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const random = Math.random() * 10
random <=5
? resolve(' ')
: reject(' ')
}, 2000)
})
}
//
const useAsync = (asyncFunction, immediate = true) => {
const [status, setStatus] = useState('idle')
const [value, setValue] = useState(null)
const [error, setError] = useState(null)
// "execute" asyncFunction
// pending, value error
// useCallback useEffect
// useEffect asyncFunction
const execute = useCallback(() => {
setStatus('pending')
setValue(null)
setError(null)
return asyncFunction()
.then(response => {
setValue(response)
setStatus('success')
})
.catch(error => {
setError(error)
setStatus('error')
})
}, [asyncFunction])
// execute
// , execute
// ,
useEffect(() => {
if (immediate) {
execute()
}
}, [execute, immediate])
return { execute, status, value, error }
}
useRequireAuth
الغرض من هذا الخطاف هو إعادة توجيه المستخدم إلى صفحة تسجيل الدخول عند تسجيل الخروج من الحساب. الخطاف الخاص بنا عبارة عن تكوين من خطاف "useAuth" و "useRouter". بالطبع ، يمكننا تنفيذ الوظيفة المطلوبة في خطاف useAuth ، ولكن بعد ذلك يتعين علينا تضمينها في مخطط التوجيه. مع التركيب ، يمكننا الحفاظ على useAuth و useRouter بسيطًا من خلال تنفيذ إعادة توجيه باستخدام خطاف مخصص.
import Dashboard from "./Dahsboard.js";
import Loading from "./Loading.js";
import { useRequireAuth } from "./use-require-auth.js";
function DashboardPage(props) {
const auth = useRequireAuth();
// auth null ( )
// false ( )
//
if (!auth) {
return <Loading />;
}
return <Dashboard auth={auth} />;
}
// (use-require-auth.js)
import { useEffect } from "react";
import { useAuth } from "./use-auth.js";
import { useRouter } from "./use-router.js";
function useRequireAuth(redirectUrl = "./signup") {
const auth = useAuth();
const router = useRouter();
// auth.user false,
// , ,
useEffect(() => {
if (auth.user === false) {
router.push(redirectUrl);
}
}, [auth, router]);
return auth;
}
useRouter
إذا كنت تستخدم React Router في عملك ، فربما تكون قد لاحظت ظهور العديد من الروابط المفيدة مؤخرًا ، مثل "useParams" و "useLocation" و "useHistory" و "useRouterMatch". دعنا نحاول أن نلفهم في خطاف واحد يعيد البيانات والطرق التي نحتاجها. سنوضح لك كيفية الجمع بين عدة خطافات وإرجاع كائن واحد يحتوي على حالاتها. بالنسبة للمكتبات مثل React Router ، من المنطقي توفير مجموعة مختارة من الخطاف المطلوب. هذا يتجنب العرض غير الضروري. لكن في بعض الأحيان نحتاج إلى كل أو معظم الخطافات المسماة.
import { useMemo } from "react";
import {
useParams,
useLocation,
useHistory,
useRouterMatch,
} from "react-router-dom";
import queryString from "query-string";
//
function MyComponent() {
//
const router = useRouter();
// (?postId=123) (/:postId)
console.log(router.query.postId);
//
console.log(router.pathname);
// router.push()
return <button onClick={(e) => router.push("./about")}>About</button>;
}
//
export function useRouter() {
const params = useParams();
const location = useLocation();
const history = useHistory();
const match = useRouterMatch();
//
// ,
return useMemo(() => {
return {
// push(), replace() pathname
push: history.push,
replace: history.replace,
pathname: location.pathname,
// "query"
// ,
// : /:topic?sort=popular -> { topic: 'react', sort: 'popular' }
query: {
...queryString.parse(location.search), //
...params,
},
// "match", "location" "history"
// React Router
match,
location,
history,
};
}, [params, match, location, history]);
}
useAuth
من الشائع أن يكون لديك العديد من المكونات التي يتم تقديمها بناءً على ما إذا كان المستخدم قد قام بتسجيل الدخول إلى الحساب. بعض هذه المكونات تستدعي طرق المصادقة مثل تسجيل الدخول ، تسجيل الخروج ، sendPasswordResetEmail ، إلخ. يعتبر خطاف "useAuth" مثاليًا لذلك ، مما يضمن أن المكون يتلقى حالة المصادقة ويعيد رسم المكون عند وجود تغييرات. بدلاً من إنشاء مثيل useAuth لكل مستخدم ، تستخدم استدعاءات الخطاف الخاصة بنا useContext للحصول على البيانات من المكون الرئيسي. يحدث السحر الحقيقي في مكون ProvideAuth ، حيث يتم تغليف جميع طرق المصادقة (في المثال الذي نستخدمه Firebase) في خطاف useProvideAuth. ثم يتم استخدام السياق لتمرير كائن المصادقة الحالي إلى المكونات الفرعية التي تستدعي useAuth.سيكون هذا أكثر منطقية بعد قراءة المثال. سبب آخر يعجبني هذا الخطاف هو تجريد مزود المصادقة الحقيقي (Firebase) ، مما يسهل إجراء التغييرات.
// App
import React from "react";
import { ProvideAuth } from "./use-auth.js";
function App(props) {
return (
<ProvideAuth>
{/*
,
Next.js, : /pages/_app.js
*/}
</ProvideAuth>
);
}
// ,
import React from "react";
import { useAuth } from "./use-auth.js";
function NavBar(props) {
// auth
const auth = useAuth();
return (
<NavbarContainer>
<Logo />
<Menu>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
{auth.user ? (
<Fragment>
<Link to="/account">Account ({auth.user.email})</Link>
<Button onClick={() => auth.signout()}>Signout</Button>
</Fragment>
) : (
<Link to="/signin">Signin</Link>
)}
</Menu>
</NavbarContainer>
);
}
// (use-auth.js)
import React, { useState, useEffect, useContext, createContext } from "react";
import * as firebase from "firebase/app";
import "firebase/auth";
// Firebase
firebase.initializeApp({
apiKey: "",
authDomain: "",
projectId: "",
appID: "",
});
const authContext = createContext();
// Provider, "auth"
// , useAuth
export const useAuth = () => {
return useContext(authContext);
};
// "auth"
//
export const useAuth = () => {
return useContext(authContext);
};
// , "auth"
function useProviderAuth() {
const [user, setUser] = useState(null);
// Firebase,
//
const signin = (email, password) => {
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then((response) => {
setUser(response.user);
return response.user;
});
};
const signup = (email, password) => {
return firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then((response) => {
setUser(response.user);
return response.user;
});
};
const signout = () => {
return firebase
.auth()
.signOut()
.then(() => {
setUser(false);
});
};
const sendPasswordResetEmail = (email) => {
return firebase
.auth()
.sendPasswordResetEmail(email)
.then(() => true);
};
const confirmPasswordReset = (code, password) => {
return firebase
.auth()
.confirmPasswordReset(code, password)
.then(() => true);
};
//
//
// ,
// "auth"
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChange((user) => {
if (user) {
setUser(user);
} else {
setUser(false);
}
});
//
return () => unsubscribe();
}, []);
// "user"
return {
user,
signin,
signup,
signout,
sendPasswordResetEmail,
confirmPasswordReset,
};
}
useEventListener
إذا كان عليك التعامل مع عدد كبير من معالجات الأحداث التي تسجل مع useEffect ، فقد ترغب في فصلهم إلى خطافات منفصلة. في المثال أدناه ، نقوم بإنشاء خطاف useEventListener الذي يتحقق من دعم addEventListener ، ويضيف معالجات ويزيلها عند الخروج.
import { useState, useRef, useEffect, useCallback } from "react";
//
function App() {
//
const [coords, setCoords] = useState({ x: 0, y: 0 });
// useCallback,
//
const handler = useCallback(
({ clientX, clientY }) => {
//
setCoords({ x: clientX, y: clientY });
},
[setCoords]
);
//
useEventListener("mousemove", handler);
return <h1> : ({(coords.x, coords.y)})</h1>;
}
//
function useEventListener(eventName, handler, element = window) {
// ,
const saveHandler = useRef();
// ref.current
//
//
//
useEffect(() => {
saveHandler.current = handler;
}, [handler]);
useEffect(
() => {
// addEventListener
const isSupported = element && element.addEventListener;
if (!isSupported) return;
// , , ref
const eventListener = (event) => saveHandler.current(event);
//
element.addEventListener(eventName, eventListener);
//
return () => {
element.removeEventListener(eventName, eventListener);
};
},
[eventName, element] //
);
}
useWhyDidYouUpdate
يتيح لك هذا الخطاف تحديد تغييرات الخاصيات التي تؤدي إلى إعادة العرض. إذا كانت الوظيفة "معقدة" وكنت متأكدًا من أنها نظيفة ، أي تُرجع نفس النتائج لنفس الخاصيات ، يمكنك استخدام مكون الترتيب الأعلى "React.memo" كما نفعل في المثال أدناه. إذا لم تتوقف عمليات التجسيد غير الضرورية بعد ذلك ، يمكنك استخدام useWhyDidYouUpdate ، الذي يُخرج إلى وحدة التحكم العناصر التي تتغير أثناء العرض ، مشيرًا إلى القيم السابقة والحالية.
import { useState, useEffect, useRef } from "react";
// , <Counter>
// React.memo,
// useWhyDidYouUpdate
const Counter = React.memo((props) => {
useWhyDidYouUpdate("Counter", props);
return <div style={props.style}>{props.count}</div>;
});
function App() {
const [count, setCount] = useState(0);
const [userId, setUserId] = useState(0);
// , , <Counter>
// , userId
// "switch user". ,
//
// ,
//
const counterStyle = {
fontSize: "3rem",
color: "red",
};
}
return (
<div>
<div className="counter">
<Counter count={count} style={counterStyle} />
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
<div className="user">
<img src={`http://i.pravatar.cc/80?img=${userId}`} />
<button onClick={() => setUserId(userId + 1)}>Switch User</button>
</div>
</div>
);
//
function useWhyDidYouUpdate(name, props) {
// "ref"
//
const prevProps = useRef();
useEffect(() => {
if (prevProps.current) {
//
const allKeys = Object.keys({ ...prevProps.current, ...props });
//
const changesObj = {};
//
allKeys.forEach((key) => {
//
if (prevProps.current[key] !== props[key]) {
// changesObj
changesObj[key] = {
from: prevProps.current[key],
to: props[key],
};
}
});
// changesObj - ,
if (object.keys(changesObj).length) {
console.log("why-did-you-update", name, changesObj);
}
}
// , prevProps
prevProps.current = props;
});
}
useDarkMode
يطبق هذا الخطاف المنطق لتبديل نظام ألوان الموقع (فاتح وداكن). يستخدم التخزين المحلي لتخزين المخطط الذي حدده المستخدم ، الوضع الافتراضي الذي تم تعيينه في المتصفح باستخدام استعلام الوسائط "نظام الألوان المفضل". لتمكين الوضع المظلم ، استخدم فئة "الوضع المظلم" لعنصر "الجسم". يوضح الخطاف أيضًا قوة التكوين. يتم تنفيذ مزامنة الحالة مع localStorage باستخدام خطاف "useLocalStorage" ، وتحديد مخطط المستخدم المفضل باستخدام الخطاف "useMedia" ، المصمم لأغراض مختلفة. ومع ذلك ، فإن تكوين هذه الخطافات ينتج عنه ربط أكثر قوة يتكون من بضعة أسطر من التعليمات البرمجية. هذا هو تقريبا نفس قوة "تكوين" الخطافات فيما يتعلق بحالة المكونات.
function App() {
const [darkMode, setDarkMode] = useDarkMode();
return (
<div>
<div className="navbar">
<Toggle darkMode={darkMode} setDarkMode={setDarkMode} />
</div>
<Content />
</div>
);
}
//
function useDarkMode() {
// "useLocalStorage"
const [enabledState, setEnableState] = useLocalStorage("dark-mode-enabled");
//
// "usePrefersDarkMode" "useMedia"
const prefersDarkMode = usePrefersDarkMode();
// enabledState , , , prefersDarkMode
const enabled =
typeof enabledState !== "undefined" ? enabledState : prefersDarkMode;
// /
useEffect(
() => {
const className = "dark-mode";
const element = window.document.body;
if (enabled) {
element.classList.add(className);
} else {
element.classList.remove(className);
}
},
[enabled] // enabled
);
//
return [enabled, setEnableState];
}
// "useMedia"
// , ,
// -
//
function usePrefersDarkMode() {
return useMedia(["(prefers-color-scheme: dark)"], [true], false);
}
useMedia
يقوم هذا الخطاف بتغليف المنطق الخاص بتعريف استعلامات الوسائط. في المثال أدناه ، نعرض عددًا مختلفًا من الأعمدة اعتمادًا على استعلام الوسائط استنادًا إلى عرض الشاشة الحالي ، ثم نضع الصورة فوق الأعمدة بحيث تكون مستوية للاختلاف في ارتفاعات العمود (لا نريد أن يكون أحد الأعمدة أطول من الآخر) ... يمكنك إنشاء خطاف يحدد عرض الشاشة مباشرة ، ولكن الخطاف الخاص بنا يسمح لك بدمج استعلامات الوسائط المحددة في JS وورقة الأنماط.
import { useState, useEffect } from "react";
function App() {
const columnCount = useMedia(
// -
["(min-width: 1500px)", "(min-width: 1000px)", "(min-width: 600px)"],
//
[5, 4, 3],
//
2
);
// ( 0)
let columnHeight = new Array(columnCount).fill(0);
// ,
let columns = new Array(columnCount).fill().map(() => []);
data.forEach((item) => {
//
const shortColumntIndex = columnHeight.indexOf(Math.min(...columnHeight));
//
columns[shortColumntIndex].push(item);
//
columnHeight[shortColumntIndex] += item.height;
});
//
return (
<div className="App">
<div className="columns is-mobile">
{columns.map((column) => (
<div className="column">
{column.map((item) => (
<div
className="image-container"
style={{
// aspect ratio
paddingTop: (item.height / item.width) * 100 + "%",
}}
>
<img src={item.image} alt="" />
</div>
))}
</div>
))}
</div>
</div>
);
}
//
function useMedia(queries, values, defaultValue) {
// -
const mediaQueryList = queries.map((q) => window.matchMedia(q));
//
const getValue = () => {
//
const index = mediaQueryList.findIndex((mql) => mql.matches);
//
return typeof values[index] !== "undefined"
? values[index]
: defaultValue;
};
//
const [value, setValue] = useState(getValue);
useEffect(
() => {
//
// : getValue useEffect,
//
//
const handler = () => setValue(getValue);
// -
mediaQueryList.forEach((mql) => mql.addEventListener(handler));
//
return () =>
mediaQueryList.forEach((mql) => mql.removeEventListener(handler));
},
[] //
);
return value;
}
useLocalStorage
تم تصميم هذا الخطاف لمزامنة الحالة مع التخزين المحلي لاستمرار الحالة عبر عمليات إعادة تحميل الصفحة. يشبه استخدام هذا الخطاف استخدام useState ، باستثناء أننا نقوم بتمرير مفتاح التخزين المحلي باعتباره المفتاح الافتراضي عند تحميل الصفحة بدلاً من تحديد قيمة أولية.
import { useState } from "react";
//
function App() {
// useState, ,
const [name, setName] = useLocalStorage("name", "Igor");
return (
<div>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
//
function useLocalStorage(key, initialValue) {
//
// useState
const [storedValue, setStoredValue] = useState(() => {
try {
//
const item = window.localStorage.getItem(key);
// initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// ,
console.error(error);
return initialValue;
}
});
// useState,
//
const setValue = (value) => {
try {
//
const valueToStore =
value instanceof Function ? value(storedValue) : value;
//
setStoredValue(valueToStore);
//
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
//
console.error(error);
}
};
return [storedValue, setValue];
}
هذا كل شيء لهذا اليوم. أتمنى أن تكون قد وجدت شيئًا مفيدًا. شكرآ لك على أهتمامك.