カスタムフック。パート1





良い一日、友達!



私はあなたの注意にトップ10のカスタムフックを提示します



目次







useMemoCompare



このフックはuseMemoに似ていますが、依存関係の配列の代わりに、以前の値と新しい値を比較する関数が渡されます。関数は、ネストされたプロパティを比較したり、オブジェクトのメソッドを呼び出したり、比較のために他のことを実行したりできます。関数がtrueを返す場合、フックは古いオブジェクトへの参照を返します。このフックは、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



非同期リクエストのステータスを表示することをお勧めします。例として

は、APIからデータをフェッチし、結果をレンダリングする前に読み込みインジケーターを表示する場合があります。もう1つの例は、フォームの送信中にボタンを無効にしてから、結果を表示することです。非同期関数の状態を追跡するために多くのuseState呼び出しでコンポーネントを汚染するのではなく、このフックを使用できます。このフックは、非同期関数を受け取り、ユーザーインターフェイスの更新に必要な「値」、「エラー」、および「ステータス」の値を返します。「status」プロパティに指定できる値は、「idle」、「pending」、「success」、「error」です。私たちのフックを使用すると、execute関数を使用して関数をすぐにまたは遅く実行できます。



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



仕事でReactRouterを使用している場合、「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を呼び出す子コンポーネントに現在の認証オブジェクトを渡します。例を読んだ後、これはより理にかなっています。このフックが好きなもう1つの理由は、実際の認証プロバイダー(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に登録する多数のイベントハンドラーを処理する必要がある場合は、それらを別々のフックに分割することをお勧めします。以下の例では、addEventListenerサポートをチェックし、ハンドラーを追加し、終了時にそれらを削除するuseEventListenerフックを作成します。

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



このフックは、サイトのカラースキーム(明るい色と暗い色)を切り替えるためのロジックを実装します。ローカルストレージを使用して、ユーザーが選択したスキームを保存します。これは、「prefers-color-scheme」メディアクエリを使用してブラウザで設定されたデフォルトのモードです。ダークモードを有効にするには、「body」要素の「dark-mode」クラスを使用します。フックはまた、構成の力を示しています。状態と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