خطافات مخصصة. الجزء الأول





يوم جيد ، أيها الأصدقاء!



أقدم انتباهكم إلى أفضل عشرة خطاطيف مخصصة .



جدول المحتويات







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];
}


هذا كل شيء لهذا اليوم. أتمنى أن تكون قد وجدت شيئًا مفيدًا. شكرآ لك على أهتمامك.



All Articles