{"id":24025122,"url":"https://github.com/tigerabrodi/50-react-hooks","last_synced_at":"2025-04-16T05:56:51.152Z","repository":{"id":223995649,"uuid":"762117778","full_name":"tigerabrodi/50-react-hooks","owner":"tigerabrodi","description":"On a quest building and documenting 50 react hooks from scratch.","archived":false,"fork":false,"pushed_at":"2024-02-24T15:50:51.000Z","size":278,"stargazers_count":28,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-29T05:05:12.334Z","etag":null,"topics":["react"],"latest_commit_sha":null,"homepage":"https://50-react-hooks.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tigerabrodi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2024-02-23T05:48:21.000Z","updated_at":"2025-03-02T10:48:42.000Z","dependencies_parsed_at":"2024-02-27T15:59:17.657Z","dependency_job_id":null,"html_url":"https://github.com/tigerabrodi/50-react-hooks","commit_stats":null,"previous_names":["narutosstudent/50-react-hooks","tigerabrodi/50-react-hooks"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tigerabrodi%2F50-react-hooks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tigerabrodi%2F50-react-hooks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tigerabrodi%2F50-react-hooks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tigerabrodi%2F50-react-hooks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tigerabrodi","download_url":"https://codeload.github.com/tigerabrodi/50-react-hooks/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249205647,"owners_count":21229965,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["react"],"created_at":"2025-01-08T15:47:33.404Z","updated_at":"2025-04-16T05:56:51.131Z","avatar_url":"https://github.com/tigerabrodi.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 50 React Hooks Explained\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 useDebounce\u003c/summary\u003e\n\n---\n\nThis one is pretty straightforward.\n\nEvery time value changes, we set a timeout to update the debounced value after the specified delay.\n\nHowever, if value keeps changing, we clear the timeout and set a new one.\n\nThis means if you keep typing for a whole second without stopping, the debounced value will only be updated once at the end.\n\n```tsx\nfunction useDebounce(value: string, delay: number) {\n  // State to hold the debounced value\n  const [debouncedValue, setDebouncedValue] = useState(value);\n\n  useEffect(() =\u003e {\n    // Handler to set debouncedValue to value after the specified delay\n    const handler = setTimeout(() =\u003e {\n      setDebouncedValue(value);\n    }, delay);\n\n    // Cleanup function to clear the timeout if the value or delay changes\n    return () =\u003e {\n      clearTimeout(handler);\n    };\n  }, [value, delay]);\n\n  return debouncedValue;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 useLocalStorage\u003c/summary\u003e\n\n---\n\nHere we start off by getting the value from localStorage, if it exists.\n\nUsing a function with the useState hook in React for the initial state is known as \"lazy initialization.\"\n\nThis method is handy when setting up the initial state takes a lot of work or relies on outside sources, like local storage. With this approach, React runs the function only once when the component first loads, enhancing performance by skipping extra work on future renders.\n\nWhen users set a new value, they may pass a function to the setValue function. This is a common pattern in React, where the new state depends on the previous state.\n\nFinally, we store the new value in localStorage.\n\n```tsx\nfunction useLocalStorage\u003cInitialValue\u003e(\n  key: string,\n  initialValue: InitialValue\n) {\n  const [storedValue, setStoredValue] = useState(() =\u003e {\n    try {\n      const item = window.localStorage.getItem(key);\n      return item ? JSON.parse(item) : initialValue;\n    } catch (error) {\n      console.log(error);\n      return initialValue;\n    }\n  });\n\n  const setValue = (\n    value: InitialValue | ((value: InitialValue) =\u003e InitialValue)\n  ) =\u003e {\n    try {\n      const valueToStore =\n        value instanceof Function ? value(storedValue) : value;\n      setStoredValue(valueToStore);\n      window.localStorage.setItem(key, JSON.stringify(valueToStore));\n    } catch (error) {\n      console.log(error);\n    }\n  };\n\n  return [storedValue, setValue];\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 useWindowSize\u003c/summary\u003e\n\n---\n\nThe initial values of windowSize should be directly coming from `window` but because we're using SSR first framework, we need to set the initial values to `null` and update them on the first render.\n\nIn an SPA application, this wouldn't be necessary.\n\nWhenever the window is resized, we update the windowSize state.\n\nFinally, we remove the event listener on cleanup.\n\nReminder: Cleanup runs before the \"new\" effect, it runs with the old values of the effect.\n\n```tsx\nfunction useWindowSize() {\n  const [windowSize, setWindowSize] = useState\u003c{\n    width: number | null;\n    height: number | null;\n  }\u003e({\n    width: null,\n    height: null,\n  });\n\n  useEffect(() =\u003e {\n    // Handler to call on window resize\n    function handleResize() {\n      // Set window width/height to state\n      setWindowSize({\n        width: window.innerWidth,\n        height: window.innerHeight,\n      });\n    }\n\n    window.addEventListener(\"resize\", handleResize);\n\n    // Call handler right away so state gets updated with initial window size\n    // Needed because we're using SSR first framework\n    handleResize();\n\n    // Remove event listener on cleanup\n    return () =\u003e window.removeEventListener(\"resize\", handleResize);\n  }, []);\n\n  return windowSize;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 usePrevious\u003c/summary\u003e\n\n---\n\n# Description\n\nThe trick with this hook is to use the `useRef` hook to store the previous value.\n\nThe reason we use refs is because they don't cause a re-render when they change, unlike state.\n\nWhen we first call useRef, this happens before the component renders for the first time, so the ref's current value is `undefined`.\n\nBecause useEffect runs after the component renders, the ref's current value will be the previous value.\n\n```tsx\nfunction usePrevious\u003cT\u003e(value: T) {\n  const ref = useRef\u003cT\u003e();\n\n  useEffect(() =\u003e {\n    ref.current = value;\n  }, [value]);\n\n  return ref.current;\n}\n```\n\n# In depth explanation\n\n## React's Update Cycle\n\nReact's update cycle can be simplified into two main phases for our context:\n\n1. **Rendering Phase:** React readies the UI based on the current state and props. This phase concludes with the virtual DOM being refreshed and arranged for applying to the actual DOM. Throughout this phase, your component function operates, executing any hooks invoked within it, such as `useState`, `useRef`, and the setup phase of `useEffect` (where you outline what the effect accomplishes, but it hasn't executed yet).\n\n2. **Commit Phase:** React applies the changes from the virtual DOM to the actual DOM, making those changes visible to the user. This is when the UI is actually updated.\n\n## Execution of `useEffect`\n\n`useEffect` is designed to run _after_ the commit phase. Its purpose is to execute side effects that should not be part of the rendering process, such as fetching data, setting up subscriptions, etc..\n\n## Why Changes in `useEffect` Don't Affect Current Cycle's DOM\n\n- **Timing:** Since `useEffect` runs after the commit phase, the DOM has already been updated with the information from the render phase by the time `useEffect` executes. React does not re-render or update the DOM again immediately after `useEffect` runs within the same cycle because React's rendering cycle has already completed.\n\n- **Intention:** This behavior is by design. React intentionally separates the effects from the rendering phase to ensure that the UI updates are efficient and predictable. If effects could modify the DOM immediately in the same cycle they run, it would lead to potential performance issues and bugs due to unexpected re-renders or state changes after the DOM has been updated.\n\n- **Ref and the DOM:** When you update `ref.current` in `useEffect`, you're modifying a value stored in memory that React uses for keeping references across renders. This update does not trigger a re-render by itself, and because `useEffect`'s changes are applied after the DOM has been updated, **there's no direct mechanism for those changes to modify the DOM until the next render cycle is triggered by state or prop changes.**\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 useIntersectionObserver\u003c/summary\u003e\n\n---\n\n`entry` gives us information about the target element's intersection with the root.\n\nThe `isIntersecting` property tells us whether the element is visible in the viewport.\n\nAs commented in the code, we copy `ref.current` to a variable to avoid a warning from React.\n\n**How it works in a nutshell:** In the useEffect, we create a new IntersectionObserver and observe the target element. We return a cleanup function that unobserves the target element.\n\n```tsx\nfunction useIntersectionObserver(options: IntersectionObserverInit = {}) {\n  const [entry, setEntry] = useState\u003cIntersectionObserverEntry | null\u003e(null);\n  const ref = useRef(null);\n\n  useEffect(() =\u003e {\n    const observer = new IntersectionObserver(\n      ([entry]) =\u003e setEntry(entry),\n      options\n    );\n\n    // Copy ref.current to a variable\n    // This is because ref.current may refer to a different element by the time the cleanup function runs\n    // This was a warning by React\n    // According to this Github issue: https://github.com/facebook/react/issues/15841\n    // It's nothing to actually worry about\n    const currentRef = ref.current;\n    if (currentRef) observer.observe(currentRef);\n\n    return () =\u003e {\n      if (currentRef) observer.unobserve(currentRef);\n    };\n  }, [options]);\n\n  return [ref, entry] as const;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 useNetworkState\u003c/summary\u003e\n\n---\n\nThis hook is used to monitor the network state of the user.\n\nIf you peek into the file `app/routes/use-network-state.tsx`, you'll see we had to author our own type for `navigator.connection` to avoid TypeScript errors.\n\nThe main key here is to `navigator`, especially `navigator.connection`.\n\nNow, to be fair, this is an experimental API, as documented on MDN: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection.\n\nHow it works in a nutshell: Similar to other hooks that use browser events, we set up event listeners for `online`, `offline`, and `change` events.\n\n`online` -\u003e when browser goes online.\n`offline` -\u003e when browser goes offline.\n`change` -\u003e when the network state changes.\n\n```tsx\nfunction useNetworkState() {\n  const [networkState, setNetworkState] = useState\u003cNetworkState\u003e({\n    online: false,\n  });\n\n  useEffect(() =\u003e {\n    const updateNetworkState = () =\u003e {\n      setNetworkState({\n        online: navigator.onLine,\n        downlink: navigator.connection?.downlink,\n        downlinkMax: navigator.connection?.downlinkMax,\n        effectiveType: navigator.connection?.effectiveType,\n        rtt: navigator.connection?.rtt,\n        saveData: navigator.connection?.saveData,\n        type: navigator.connection?.type,\n      });\n    };\n\n    // Call the function once to get the initial state\n    updateNetworkState();\n\n    window.addEventListener(\"online\", updateNetworkState);\n    window.addEventListener(\"offline\", updateNetworkState);\n    navigator.connection?.addEventListener(\"change\", updateNetworkState);\n\n    return () =\u003e {\n      window.removeEventListener(\"online\", updateNetworkState);\n      window.removeEventListener(\"offline\", updateNetworkState);\n      navigator.connection?.removeEventListener(\"change\", updateNetworkState);\n    };\n  }, []);\n\n  return networkState;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 useMediaQuery\u003c/summary\u003e\n\n---\n\nWe set up a listener for the media query and update the matches state whenever the media query changes.\n\nThe matches state is initially set to false, and it is set to true when the media query matches.\n\nWe also return a cleanup function that removes the event listener when the component unmounts.\n\nThis hook is useful for conditionally rendering content based on the state of a media query.\n\nFor example, you can use it to show or hide certain elements based on the screen size.\n\n```tsx\nfunction useMediaQuery(query: string) {\n  const [matches, setMatches] = useState(false);\n\n  useEffect(() =\u003e {\n    const mediaQuery = window.matchMedia(query);\n    setMatches(mediaQuery.matches);\n\n    const listener = (event: MediaQueryListEvent) =\u003e {\n      setMatches(event.matches);\n    };\n\n    mediaQuery.addEventListener(\"change\", listener);\n\n    return () =\u003e {\n      mediaQuery.removeEventListener(\"change\", listener);\n    };\n  }, [query]);\n\n  return matches;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 useOrientation\u003c/summary\u003e\n\n---\n\nThis hook is used to monitor the orientation of the user's device.\n\nFor example, you can use it to change the layout of your app based on the orientation of the device.\n\nOrientation means whether the device is in portrait or landscape mode, when e.g. holding your phone, you can hold it vertically or horizontally.\n\nWe set up an event listener for the `orientationchange` event and update the orientation state whenever the orientation changes.\n\n```tsx\nfunction useOrientation() {\n  const [orientation, setOrientation] = useState\u003cScreenOrientation | null\u003e(\n    null\n  );\n\n  useEffect(() =\u003e {\n    const handleOrientationChange = () =\u003e {\n      setOrientation(window.screen.orientation);\n    };\n\n    // Set the initial orientation\n    handleOrientationChange();\n\n    window.addEventListener(\"orientationchange\", handleOrientationChange);\n\n    return () =\u003e {\n      window.removeEventListener(\"orientationchange\", handleOrientationChange);\n    };\n  }, []);\n\n  return orientation;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 useSessionStorage\u003c/summary\u003e\n\n---\n\nThis hook is similar to the `useLocalStorage` hook, but it uses `sessionStorage` instead of `localStorage`.\n\n```tsx\nfunction useSessionStorage\u003cInitialValue\u003e(\n  key: string,\n  initialValue: InitialValue\n) {\n  const [value, setValue] = useState\u003cInitialValue\u003e(() =\u003e {\n    if (typeof window === \"undefined\") {\n      return initialValue;\n    }\n\n    const storedValue = sessionStorage.getItem(key);\n    return storedValue !== null ? JSON.parse(storedValue) : initialValue;\n  });\n\n  // Set Inital Value\n  useEffect(() =\u003e {\n    setValue(\n      JSON.parse(sessionStorage.getItem(key) || JSON.stringify(initialValue))\n    );\n  }, [initialValue, key]);\n\n  useEffect(() =\u003e {\n    sessionStorage.setItem(key, JSON.stringify(value));\n  }, [key, value]);\n\n  return [value, setValue] as const;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 usePreferredLanguage\u003c/summary\u003e\n\n---\n\nThis hook is used to get the user's preferred language.\n\nIt uses the `navigator.language` property to get the user's preferred language.\n\nEvery time the user's preferred language changes, the `languagechange` event is fired, and we update the language state.\n\n```tsx\nfunction usePreferredLanguage() {\n  const [language, setLanguage] = useState\u003cstring | null\u003e(null);\n\n  useEffect(() =\u003e {\n    const handler = () =\u003e {\n      setLanguage(navigator.language);\n    };\n\n    // Set the initial language\n    handler();\n\n    window.addEventListener(\"languagechange\", handler);\n\n    return () =\u003e {\n      window.removeEventListener(\"languagechange\", handler);\n    };\n  }, []);\n\n  return language;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 useFetch\u003c/summary\u003e\n\n---\n\nThis hook is used to fetch data from an API.\n\nIt uses the `fetch` API to make a request to the specified URL.\n\nIt's gonna fetch the data every time the URL changes.\n\nThe `useEffect` hook is used to fetch the data when the URL changes.\n\nIt returns an object with the data, loading state, and error.\n\nA common bad practice is to use boolean for the loading state, status is a better approach and more accurate\n\n```tsx\nexport function useFetch\u003cData\u003e(url: string) {\n  const [status, setStatus] = useState\u003c\n    \"idle\" | \"loading\" | \"error\" | \"success\"\n  \u003e(\"idle\");\n  const [data, setData] = useState\u003cData | null\u003e(null);\n  const [error, setError] = useState\u003cunknown | null\u003e(null);\n\n  useEffect(() =\u003e {\n    if (!url) return;\n    setStatus(\"loading\");\n\n    fetch(url)\n      .then((res) =\u003e res.json())\n      .then((data) =\u003e {\n        setData(data as Data);\n        setStatus(\"success\");\n      })\n      .catch((error) =\u003e {\n        setError(error);\n        setStatus(\"error\");\n      });\n  }, [url]);\n\n  return { error, isLoading: status === \"loading\", data };\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 useContinuousRetry\u003c/summary\u003e\n\n---\n\nThis hook is used to retry a function continuously until it returns true.\n\nThe nice part here is that you can specify whatever you want to retry in the callback function.\n\nAs commented why we need `useCallback`, it's because we want to retain the same reference across renders, unless its dependencies change.\n\nThe callback function would change if e.g. the state inside the callback changes.\n\nLet's look at the route for example:\n\n```tsx\nexport default function UseContinuousRetryRoute() {\n  const [count, setCount] = useState(0);\n  const hasResolved = useContinuousRetry(() =\u003e count \u003e 10, 1000, {\n    maxRetries: 15,\n  });\n\n  return (\n    // ...\n  );\n}\n```\n\nIf `count` changes, the callback function would change, and `attemptRetry` would be re-created as a result.\n\nIn the useEffect of the hook, we clean up the interval when the component unmounts.\n\n```tsx\ninterface UseContinuousRetryOptions {\n  interval?: number;\n  maxRetries?: number;\n}\n\nfunction useContinuousRetry(\n  callback: () =\u003e boolean,\n  interval: number = 100,\n  options: UseContinuousRetryOptions = {}\n) {\n  const [hasResolved, setHasResolved] = useState(false);\n  const [retryCount, setRetryCount] = useState(0);\n\n  const maxRetries = options.maxRetries;\n\n  // Using useCallback, the function retains the same reference across renders,\n  // unless its dependencies change. This stability prevents unnecessary re-executions\n  // of effects or callbacks that depend on this function, controlling the retry behavior as expected.\n  // Without useCallback, the function would be re-created on every render, even if its dependencies haven't changed.\n  const attemptRetry = useCallback(() =\u003e {\n    if (callback()) {\n      setHasResolved(true);\n      return;\n    }\n    setRetryCount((count) =\u003e count + 1);\n  }, [callback]);\n\n  useEffect(() =\u003e {\n    const hasRetryReachedLimit =\n      maxRetries !== undefined \u0026\u0026 retryCount \u003e= maxRetries;\n    if (hasResolved || hasRetryReachedLimit) {\n      return;\n    }\n\n    const id = setInterval(attemptRetry, interval);\n\n    return () =\u003e clearInterval(id);\n  }, [attemptRetry, hasResolved, interval, maxRetries, retryCount]);\n\n  return hasResolved;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 useVisibilityChange\u003c/summary\u003e\n\n---\n\nThis hook is used to monitor the visibility state of the document.\n\nIt's useful for when you want to pause or resume a video when the user switches tabs, for example.\n\nThe `documentVisible` state is initially set to `null` and it is set to `true` when the document becomes visible.\n\nBecause we're using an SSR first framework, we need to set the initial state in the `useEffect` hook.\n\nWe also use `document.addEventListener` to listen for the `visibilitychange` event and update the `documentVisible` state accordingly.\n\nFinally, we use `document.removeEventListener` to remove the event listener when the component unmounts.\n\n```tsx\nfunction useVisibilityChange() {\n  const [documentVisible, setDocumentVisible] = useState\u003cboolean | null\u003e(null);\n\n  useEffect(() =\u003e {\n    const handleVisibilityChange = () =\u003e {\n      setDocumentVisible(document.visibilityState === \"visible\");\n    };\n\n    // Set the initial state\n    handleVisibilityChange();\n\n    document.addEventListener(\"visibilitychange\", handleVisibilityChange);\n\n    return () =\u003e {\n      document.removeEventListener(\"visibilitychange\", handleVisibilityChange);\n    };\n  }, []);\n\n  return documentVisible;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 useScript\u003c/summary\u003e\n\n---\n\nThis hook is used to load an external script.\n\nIt's useful for when you want to load a third-party script, like Google Analytics, for example.\n\nThe `status` state is initially set to `loading` and it is set to `ready` when the script is loaded successfully, otherwise it is set to `error`.\n\nIf script doesn't exist, we create a new script element and append it to the body.\n\nIf it does exist, we set the status to `ready`.\n\nWe also use `script.addEventListener` to listen for the `load` and `error` events and update the `status` accordingly.\n\nFinally, we use `script.removeEventListener` to remove the event listener when the component unmounts.\n\n```tsx\ntype ScriptStatus = \"loading\" | \"ready\" | \"error\";\n\nfunction useScript(\n  src: string,\n  options?: { removeOnUnmount?: boolean }\n): ScriptStatus {\n  const [status, setStatus] = useState\u003cScriptStatus\u003e(src ? \"loading\" : \"error\");\n\n  const setReady = () =\u003e setStatus(\"ready\");\n  const setError = () =\u003e setStatus(\"error\");\n\n  useEffect(() =\u003e {\n    let script: HTMLScriptElement | null = document.querySelector(\n      `script[src=\"${src}\"]`\n    );\n\n    if (!script) {\n      script = document.createElement(\"script\");\n      script.src = src;\n      script.async = true;\n      document.body.appendChild(script);\n\n      script.addEventListener(\"load\", setReady);\n      script.addEventListener(\"error\", setError);\n    } else {\n      setStatus(\"ready\");\n    }\n\n    return () =\u003e {\n      if (script) {\n        script.removeEventListener(\"load\", setReady);\n        script.removeEventListener(\"error\", setError);\n\n        if (options?.removeOnUnmount) {\n          script.remove();\n        }\n      }\n    };\n  }, [src, options?.removeOnUnmount]);\n\n  return status;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e🍿 useRenderInfo\u003c/summary\u003e\n\n---\n\nThis hook is used to log information about the component's renders.\n\nDuring development, you may see 2 renders of a component and wonder why it's happening. It's because of React's StrictMode.\n\nThis hook is useful when you want to log information about the component's renders e.g. when debugging performance issues.\n\nWe use useRef to store the render information and log it to the console. Refs are perfect for this because they don't cause a re-render when they change, unlike state. And they persist across renders.\n\nThe reason we don't need useEffect here is because we don't need to run the effect after the component renders, we just need to run it once when the component mounts, and as mentioned, to not lose the state, we use useRef.\n\nAnother thing to not get confused: Because `info.timestamp` is updated after `info.sinceLast` is calculated, `info.sinceLast` will always be the time since the previous render, not the current one. That's why \"now\" is used when `info.timestamp` is null, which is the first render.\n\n```tsx\ninterface RenderInfo {\n  readonly module: string;\n  renders: number;\n  timestamp: null | number;\n  sinceLast: null | number | \"[now]\";\n}\n\nconst useRenderInfo = (\n  moduleName: string = \"Unknown component\",\n  log: boolean = true\n) =\u003e {\n  const { current: info } = useRef\u003cRenderInfo\u003e({\n    module: moduleName,\n    renders: 0,\n    timestamp: null,\n    sinceLast: null,\n  });\n\n  const now = Date.now();\n\n  info.renders += 1;\n  info.sinceLast = info.timestamp ? (now - info.timestamp) / 1000 : \"[now]\";\n  info.timestamp = now;\n\n  if (log) {\n    console.group(`${moduleName} info`);\n    console.log(\n      `Render no: ${info.renders}${\n        info.renders \u003e 1 ? `, ${info.sinceLast}s since last render` : \"\"\n      }`\n    );\n    console.dir(info);\n    console.groupEnd();\n  }\n\n  return info;\n};\n```\n\n\u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftigerabrodi%2F50-react-hooks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftigerabrodi%2F50-react-hooks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftigerabrodi%2F50-react-hooks/lists"}