├── .eslintrc.cjs
├── .gitignore
├── README.md
├── app
├── root.tsx
├── routes
│ ├── _index.tsx
│ ├── use-continuous-retry.tsx
│ ├── use-debounce.tsx
│ ├── use-fetch.tsx
│ ├── use-intersection-observer.tsx
│ ├── use-local-storage.tsx
│ ├── use-media-query.tsx
│ ├── use-network-state.tsx
│ ├── use-orientation.tsx
│ ├── use-preferred-language.tsx
│ ├── use-previous.tsx
│ ├── use-render-info.tsx
│ ├── use-script.tsx
│ ├── use-session-storage.tsx
│ ├── use-visibility-change.tsx
│ └── use-window-size.tsx
└── tailwind.css
├── package-lock.json
├── package.json
├── public
└── favicon.ico
├── remix.config.js
├── remix.env.d.ts
├── tailwind.config.ts
└── tsconfig.json
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('eslint').Linter.Config} */
2 | module.exports = {
3 | extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
4 | };
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /build
5 | /public/build
6 | .env
7 | .vercel
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 50 React Hooks Explained
2 |
3 |
4 | 🍿 useDebounce
5 |
6 | ---
7 |
8 | This one is pretty straightforward.
9 |
10 | Every time value changes, we set a timeout to update the debounced value after the specified delay.
11 |
12 | However, if value keeps changing, we clear the timeout and set a new one.
13 |
14 | This means if you keep typing for a whole second without stopping, the debounced value will only be updated once at the end.
15 |
16 | ```tsx
17 | function useDebounce(value: string, delay: number) {
18 | // State to hold the debounced value
19 | const [debouncedValue, setDebouncedValue] = useState(value);
20 |
21 | useEffect(() => {
22 | // Handler to set debouncedValue to value after the specified delay
23 | const handler = setTimeout(() => {
24 | setDebouncedValue(value);
25 | }, delay);
26 |
27 | // Cleanup function to clear the timeout if the value or delay changes
28 | return () => {
29 | clearTimeout(handler);
30 | };
31 | }, [value, delay]);
32 |
33 | return debouncedValue;
34 | }
35 | ```
36 |
37 |
38 |
39 |
40 | 🍿 useLocalStorage
41 |
42 | ---
43 |
44 | Here we start off by getting the value from localStorage, if it exists.
45 |
46 | Using a function with the useState hook in React for the initial state is known as "lazy initialization."
47 |
48 | This 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.
49 |
50 | When 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.
51 |
52 | Finally, we store the new value in localStorage.
53 |
54 | ```tsx
55 | function useLocalStorage(
56 | key: string,
57 | initialValue: InitialValue
58 | ) {
59 | const [storedValue, setStoredValue] = useState(() => {
60 | try {
61 | const item = window.localStorage.getItem(key);
62 | return item ? JSON.parse(item) : initialValue;
63 | } catch (error) {
64 | console.log(error);
65 | return initialValue;
66 | }
67 | });
68 |
69 | const setValue = (
70 | value: InitialValue | ((value: InitialValue) => InitialValue)
71 | ) => {
72 | try {
73 | const valueToStore =
74 | value instanceof Function ? value(storedValue) : value;
75 | setStoredValue(valueToStore);
76 | window.localStorage.setItem(key, JSON.stringify(valueToStore));
77 | } catch (error) {
78 | console.log(error);
79 | }
80 | };
81 |
82 | return [storedValue, setValue];
83 | }
84 | ```
85 |
86 |
87 |
88 |
89 | 🍿 useWindowSize
90 |
91 | ---
92 |
93 | The 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.
94 |
95 | In an SPA application, this wouldn't be necessary.
96 |
97 | Whenever the window is resized, we update the windowSize state.
98 |
99 | Finally, we remove the event listener on cleanup.
100 |
101 | Reminder: Cleanup runs before the "new" effect, it runs with the old values of the effect.
102 |
103 | ```tsx
104 | function useWindowSize() {
105 | const [windowSize, setWindowSize] = useState<{
106 | width: number | null;
107 | height: number | null;
108 | }>({
109 | width: null,
110 | height: null,
111 | });
112 |
113 | useEffect(() => {
114 | // Handler to call on window resize
115 | function handleResize() {
116 | // Set window width/height to state
117 | setWindowSize({
118 | width: window.innerWidth,
119 | height: window.innerHeight,
120 | });
121 | }
122 |
123 | window.addEventListener("resize", handleResize);
124 |
125 | // Call handler right away so state gets updated with initial window size
126 | // Needed because we're using SSR first framework
127 | handleResize();
128 |
129 | // Remove event listener on cleanup
130 | return () => window.removeEventListener("resize", handleResize);
131 | }, []);
132 |
133 | return windowSize;
134 | }
135 | ```
136 |
137 |
138 |
139 |
140 | 🍿 usePrevious
141 |
142 | ---
143 |
144 | # Description
145 |
146 | The trick with this hook is to use the `useRef` hook to store the previous value.
147 |
148 | The reason we use refs is because they don't cause a re-render when they change, unlike state.
149 |
150 | When we first call useRef, this happens before the component renders for the first time, so the ref's current value is `undefined`.
151 |
152 | Because useEffect runs after the component renders, the ref's current value will be the previous value.
153 |
154 | ```tsx
155 | function usePrevious(value: T) {
156 | const ref = useRef();
157 |
158 | useEffect(() => {
159 | ref.current = value;
160 | }, [value]);
161 |
162 | return ref.current;
163 | }
164 | ```
165 |
166 | # In depth explanation
167 |
168 | ## React's Update Cycle
169 |
170 | React's update cycle can be simplified into two main phases for our context:
171 |
172 | 1. **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).
173 |
174 | 2. **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.
175 |
176 | ## Execution of `useEffect`
177 |
178 | `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..
179 |
180 | ## Why Changes in `useEffect` Don't Affect Current Cycle's DOM
181 |
182 | - **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.
183 |
184 | - **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.
185 |
186 | - **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.**
187 |
188 |
189 |
190 |
191 | 🍿 useIntersectionObserver
192 |
193 | ---
194 |
195 | `entry` gives us information about the target element's intersection with the root.
196 |
197 | The `isIntersecting` property tells us whether the element is visible in the viewport.
198 |
199 | As commented in the code, we copy `ref.current` to a variable to avoid a warning from React.
200 |
201 | **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.
202 |
203 | ```tsx
204 | function useIntersectionObserver(options: IntersectionObserverInit = {}) {
205 | const [entry, setEntry] = useState(null);
206 | const ref = useRef(null);
207 |
208 | useEffect(() => {
209 | const observer = new IntersectionObserver(
210 | ([entry]) => setEntry(entry),
211 | options
212 | );
213 |
214 | // Copy ref.current to a variable
215 | // This is because ref.current may refer to a different element by the time the cleanup function runs
216 | // This was a warning by React
217 | // According to this Github issue: https://github.com/facebook/react/issues/15841
218 | // It's nothing to actually worry about
219 | const currentRef = ref.current;
220 | if (currentRef) observer.observe(currentRef);
221 |
222 | return () => {
223 | if (currentRef) observer.unobserve(currentRef);
224 | };
225 | }, [options]);
226 |
227 | return [ref, entry] as const;
228 | }
229 | ```
230 |
231 |
232 |
233 |
234 | 🍿 useNetworkState
235 |
236 | ---
237 |
238 | This hook is used to monitor the network state of the user.
239 |
240 | If 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.
241 |
242 | The main key here is to `navigator`, especially `navigator.connection`.
243 |
244 | Now, to be fair, this is an experimental API, as documented on MDN: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection.
245 |
246 | How it works in a nutshell: Similar to other hooks that use browser events, we set up event listeners for `online`, `offline`, and `change` events.
247 |
248 | `online` -> when browser goes online.
249 | `offline` -> when browser goes offline.
250 | `change` -> when the network state changes.
251 |
252 | ```tsx
253 | function useNetworkState() {
254 | const [networkState, setNetworkState] = useState({
255 | online: false,
256 | });
257 |
258 | useEffect(() => {
259 | const updateNetworkState = () => {
260 | setNetworkState({
261 | online: navigator.onLine,
262 | downlink: navigator.connection?.downlink,
263 | downlinkMax: navigator.connection?.downlinkMax,
264 | effectiveType: navigator.connection?.effectiveType,
265 | rtt: navigator.connection?.rtt,
266 | saveData: navigator.connection?.saveData,
267 | type: navigator.connection?.type,
268 | });
269 | };
270 |
271 | // Call the function once to get the initial state
272 | updateNetworkState();
273 |
274 | window.addEventListener("online", updateNetworkState);
275 | window.addEventListener("offline", updateNetworkState);
276 | navigator.connection?.addEventListener("change", updateNetworkState);
277 |
278 | return () => {
279 | window.removeEventListener("online", updateNetworkState);
280 | window.removeEventListener("offline", updateNetworkState);
281 | navigator.connection?.removeEventListener("change", updateNetworkState);
282 | };
283 | }, []);
284 |
285 | return networkState;
286 | }
287 | ```
288 |
289 |
290 |
291 |
292 | 🍿 useMediaQuery
293 |
294 | ---
295 |
296 | We set up a listener for the media query and update the matches state whenever the media query changes.
297 |
298 | The matches state is initially set to false, and it is set to true when the media query matches.
299 |
300 | We also return a cleanup function that removes the event listener when the component unmounts.
301 |
302 | This hook is useful for conditionally rendering content based on the state of a media query.
303 |
304 | For example, you can use it to show or hide certain elements based on the screen size.
305 |
306 | ```tsx
307 | function useMediaQuery(query: string) {
308 | const [matches, setMatches] = useState(false);
309 |
310 | useEffect(() => {
311 | const mediaQuery = window.matchMedia(query);
312 | setMatches(mediaQuery.matches);
313 |
314 | const listener = (event: MediaQueryListEvent) => {
315 | setMatches(event.matches);
316 | };
317 |
318 | mediaQuery.addEventListener("change", listener);
319 |
320 | return () => {
321 | mediaQuery.removeEventListener("change", listener);
322 | };
323 | }, [query]);
324 |
325 | return matches;
326 | }
327 | ```
328 |
329 |
330 |
331 |
332 | 🍿 useOrientation
333 |
334 | ---
335 |
336 | This hook is used to monitor the orientation of the user's device.
337 |
338 | For example, you can use it to change the layout of your app based on the orientation of the device.
339 |
340 | Orientation means whether the device is in portrait or landscape mode, when e.g. holding your phone, you can hold it vertically or horizontally.
341 |
342 | We set up an event listener for the `orientationchange` event and update the orientation state whenever the orientation changes.
343 |
344 | ```tsx
345 | function useOrientation() {
346 | const [orientation, setOrientation] = useState(
347 | null
348 | );
349 |
350 | useEffect(() => {
351 | const handleOrientationChange = () => {
352 | setOrientation(window.screen.orientation);
353 | };
354 |
355 | // Set the initial orientation
356 | handleOrientationChange();
357 |
358 | window.addEventListener("orientationchange", handleOrientationChange);
359 |
360 | return () => {
361 | window.removeEventListener("orientationchange", handleOrientationChange);
362 | };
363 | }, []);
364 |
365 | return orientation;
366 | }
367 | ```
368 |
369 |
370 |
371 |
372 | 🍿 useSessionStorage
373 |
374 | ---
375 |
376 | This hook is similar to the `useLocalStorage` hook, but it uses `sessionStorage` instead of `localStorage`.
377 |
378 | ```tsx
379 | function useSessionStorage(
380 | key: string,
381 | initialValue: InitialValue
382 | ) {
383 | const [value, setValue] = useState(() => {
384 | if (typeof window === "undefined") {
385 | return initialValue;
386 | }
387 |
388 | const storedValue = sessionStorage.getItem(key);
389 | return storedValue !== null ? JSON.parse(storedValue) : initialValue;
390 | });
391 |
392 | // Set Inital Value
393 | useEffect(() => {
394 | setValue(
395 | JSON.parse(sessionStorage.getItem(key) || JSON.stringify(initialValue))
396 | );
397 | }, [initialValue, key]);
398 |
399 | useEffect(() => {
400 | sessionStorage.setItem(key, JSON.stringify(value));
401 | }, [key, value]);
402 |
403 | return [value, setValue] as const;
404 | }
405 | ```
406 |
407 |
408 |
409 |
410 | 🍿 usePreferredLanguage
411 |
412 | ---
413 |
414 | This hook is used to get the user's preferred language.
415 |
416 | It uses the `navigator.language` property to get the user's preferred language.
417 |
418 | Every time the user's preferred language changes, the `languagechange` event is fired, and we update the language state.
419 |
420 | ```tsx
421 | function usePreferredLanguage() {
422 | const [language, setLanguage] = useState(null);
423 |
424 | useEffect(() => {
425 | const handler = () => {
426 | setLanguage(navigator.language);
427 | };
428 |
429 | // Set the initial language
430 | handler();
431 |
432 | window.addEventListener("languagechange", handler);
433 |
434 | return () => {
435 | window.removeEventListener("languagechange", handler);
436 | };
437 | }, []);
438 |
439 | return language;
440 | }
441 | ```
442 |
443 |
444 |
445 |
446 | 🍿 useFetch
447 |
448 | ---
449 |
450 | This hook is used to fetch data from an API.
451 |
452 | It uses the `fetch` API to make a request to the specified URL.
453 |
454 | It's gonna fetch the data every time the URL changes.
455 |
456 | The `useEffect` hook is used to fetch the data when the URL changes.
457 |
458 | It returns an object with the data, loading state, and error.
459 |
460 | A common bad practice is to use boolean for the loading state, status is a better approach and more accurate
461 |
462 | ```tsx
463 | export function useFetch(url: string) {
464 | const [status, setStatus] = useState<
465 | "idle" | "loading" | "error" | "success"
466 | >("idle");
467 | const [data, setData] = useState(null);
468 | const [error, setError] = useState(null);
469 |
470 | useEffect(() => {
471 | if (!url) return;
472 | setStatus("loading");
473 |
474 | fetch(url)
475 | .then((res) => res.json())
476 | .then((data) => {
477 | setData(data as Data);
478 | setStatus("success");
479 | })
480 | .catch((error) => {
481 | setError(error);
482 | setStatus("error");
483 | });
484 | }, [url]);
485 |
486 | return { error, isLoading: status === "loading", data };
487 | }
488 | ```
489 |
490 |
491 |
492 |
493 | 🍿 useContinuousRetry
494 |
495 | ---
496 |
497 | This hook is used to retry a function continuously until it returns true.
498 |
499 | The nice part here is that you can specify whatever you want to retry in the callback function.
500 |
501 | As commented why we need `useCallback`, it's because we want to retain the same reference across renders, unless its dependencies change.
502 |
503 | The callback function would change if e.g. the state inside the callback changes.
504 |
505 | Let's look at the route for example:
506 |
507 | ```tsx
508 | export default function UseContinuousRetryRoute() {
509 | const [count, setCount] = useState(0);
510 | const hasResolved = useContinuousRetry(() => count > 10, 1000, {
511 | maxRetries: 15,
512 | });
513 |
514 | return (
515 | // ...
516 | );
517 | }
518 | ```
519 |
520 | If `count` changes, the callback function would change, and `attemptRetry` would be re-created as a result.
521 |
522 | In the useEffect of the hook, we clean up the interval when the component unmounts.
523 |
524 | ```tsx
525 | interface UseContinuousRetryOptions {
526 | interval?: number;
527 | maxRetries?: number;
528 | }
529 |
530 | function useContinuousRetry(
531 | callback: () => boolean,
532 | interval: number = 100,
533 | options: UseContinuousRetryOptions = {}
534 | ) {
535 | const [hasResolved, setHasResolved] = useState(false);
536 | const [retryCount, setRetryCount] = useState(0);
537 |
538 | const maxRetries = options.maxRetries;
539 |
540 | // Using useCallback, the function retains the same reference across renders,
541 | // unless its dependencies change. This stability prevents unnecessary re-executions
542 | // of effects or callbacks that depend on this function, controlling the retry behavior as expected.
543 | // Without useCallback, the function would be re-created on every render, even if its dependencies haven't changed.
544 | const attemptRetry = useCallback(() => {
545 | if (callback()) {
546 | setHasResolved(true);
547 | return;
548 | }
549 | setRetryCount((count) => count + 1);
550 | }, [callback]);
551 |
552 | useEffect(() => {
553 | const hasRetryReachedLimit =
554 | maxRetries !== undefined && retryCount >= maxRetries;
555 | if (hasResolved || hasRetryReachedLimit) {
556 | return;
557 | }
558 |
559 | const id = setInterval(attemptRetry, interval);
560 |
561 | return () => clearInterval(id);
562 | }, [attemptRetry, hasResolved, interval, maxRetries, retryCount]);
563 |
564 | return hasResolved;
565 | }
566 | ```
567 |
568 |
569 |
570 |
571 | 🍿 useVisibilityChange
572 |
573 | ---
574 |
575 | This hook is used to monitor the visibility state of the document.
576 |
577 | It's useful for when you want to pause or resume a video when the user switches tabs, for example.
578 |
579 | The `documentVisible` state is initially set to `null` and it is set to `true` when the document becomes visible.
580 |
581 | Because we're using an SSR first framework, we need to set the initial state in the `useEffect` hook.
582 |
583 | We also use `document.addEventListener` to listen for the `visibilitychange` event and update the `documentVisible` state accordingly.
584 |
585 | Finally, we use `document.removeEventListener` to remove the event listener when the component unmounts.
586 |
587 | ```tsx
588 | function useVisibilityChange() {
589 | const [documentVisible, setDocumentVisible] = useState(null);
590 |
591 | useEffect(() => {
592 | const handleVisibilityChange = () => {
593 | setDocumentVisible(document.visibilityState === "visible");
594 | };
595 |
596 | // Set the initial state
597 | handleVisibilityChange();
598 |
599 | document.addEventListener("visibilitychange", handleVisibilityChange);
600 |
601 | return () => {
602 | document.removeEventListener("visibilitychange", handleVisibilityChange);
603 | };
604 | }, []);
605 |
606 | return documentVisible;
607 | }
608 | ```
609 |
610 |
611 |
612 |
613 | 🍿 useScript
614 |
615 | ---
616 |
617 | This hook is used to load an external script.
618 |
619 | It's useful for when you want to load a third-party script, like Google Analytics, for example.
620 |
621 | The `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`.
622 |
623 | If script doesn't exist, we create a new script element and append it to the body.
624 |
625 | If it does exist, we set the status to `ready`.
626 |
627 | We also use `script.addEventListener` to listen for the `load` and `error` events and update the `status` accordingly.
628 |
629 | Finally, we use `script.removeEventListener` to remove the event listener when the component unmounts.
630 |
631 | ```tsx
632 | type ScriptStatus = "loading" | "ready" | "error";
633 |
634 | function useScript(
635 | src: string,
636 | options?: { removeOnUnmount?: boolean }
637 | ): ScriptStatus {
638 | const [status, setStatus] = useState(src ? "loading" : "error");
639 |
640 | const setReady = () => setStatus("ready");
641 | const setError = () => setStatus("error");
642 |
643 | useEffect(() => {
644 | let script: HTMLScriptElement | null = document.querySelector(
645 | `script[src="${src}"]`
646 | );
647 |
648 | if (!script) {
649 | script = document.createElement("script");
650 | script.src = src;
651 | script.async = true;
652 | document.body.appendChild(script);
653 |
654 | script.addEventListener("load", setReady);
655 | script.addEventListener("error", setError);
656 | } else {
657 | setStatus("ready");
658 | }
659 |
660 | return () => {
661 | if (script) {
662 | script.removeEventListener("load", setReady);
663 | script.removeEventListener("error", setError);
664 |
665 | if (options?.removeOnUnmount) {
666 | script.remove();
667 | }
668 | }
669 | };
670 | }, [src, options?.removeOnUnmount]);
671 |
672 | return status;
673 | }
674 | ```
675 |
676 |
677 |
678 |
679 | 🍿 useRenderInfo
680 |
681 | ---
682 |
683 | This hook is used to log information about the component's renders.
684 |
685 | During development, you may see 2 renders of a component and wonder why it's happening. It's because of React's StrictMode.
686 |
687 | This hook is useful when you want to log information about the component's renders e.g. when debugging performance issues.
688 |
689 | We 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.
690 |
691 | The 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.
692 |
693 | Another 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.
694 |
695 | ```tsx
696 | interface RenderInfo {
697 | readonly module: string;
698 | renders: number;
699 | timestamp: null | number;
700 | sinceLast: null | number | "[now]";
701 | }
702 |
703 | const useRenderInfo = (
704 | moduleName: string = "Unknown component",
705 | log: boolean = true
706 | ) => {
707 | const { current: info } = useRef({
708 | module: moduleName,
709 | renders: 0,
710 | timestamp: null,
711 | sinceLast: null,
712 | });
713 |
714 | const now = Date.now();
715 |
716 | info.renders += 1;
717 | info.sinceLast = info.timestamp ? (now - info.timestamp) / 1000 : "[now]";
718 | info.timestamp = now;
719 |
720 | if (log) {
721 | console.group(`${moduleName} info`);
722 | console.log(
723 | `Render no: ${info.renders}${
724 | info.renders > 1 ? `, ${info.sinceLast}s since last render` : ""
725 | }`
726 | );
727 | console.dir(info);
728 | console.groupEnd();
729 | }
730 |
731 | return info;
732 | };
733 | ```
734 |
735 |
736 |
--------------------------------------------------------------------------------
/app/root.tsx:
--------------------------------------------------------------------------------
1 | import { cssBundleHref } from "@remix-run/css-bundle";
2 | import {
3 | Link,
4 | Links,
5 | LiveReload,
6 | Meta,
7 | Outlet,
8 | Scripts,
9 | ScrollRestoration,
10 | } from "@remix-run/react";
11 | import { Analytics } from "@vercel/analytics/react";
12 | import type { LinksFunction } from "@vercel/remix";
13 |
14 | import styles from "./tailwind.css";
15 |
16 | export const links: LinksFunction = () => [
17 | ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
18 | { rel: "stylesheet", href: styles },
19 | ];
20 |
21 | export default function App() {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/app/routes/_index.tsx:
--------------------------------------------------------------------------------
1 | import type { MetaFunction } from "@vercel/remix";
2 |
3 | export const meta: MetaFunction = () => {
4 | return [
5 | { title: "50 React hooks from scratch" },
6 | {
7 | name: "description",
8 | content: "Building and documenting 50 react hooks from scratch",
9 | },
10 | ];
11 | };
12 |
13 | const hooks = [
14 | {
15 | name: "useDebounce",
16 | description:
17 | "Delay the execution of function or state update with useDebounce.",
18 | id: "use-debounce",
19 | },
20 | {
21 | name: "useLocalStorage",
22 | description:
23 | "Persist state to local storage and keep it synchronized with useLocalStorage.",
24 | id: "use-local-storage",
25 | },
26 | {
27 | name: "useWindowSize",
28 | description:
29 | "Track the dimensions of the browser window with useWindowSize.",
30 | id: "use-window-size",
31 | },
32 | {
33 | name: "usePrevious",
34 | description: "Access the previous value of a state with usePrevious.",
35 | id: "use-previous",
36 | },
37 | {
38 | name: "useIntersectionObserver",
39 | description:
40 | "Track the visibility of an element with useIntersectionObserver.",
41 | id: "use-intersection-observer",
42 | },
43 | {
44 | name: "useNetworkState",
45 | description:
46 | "Monitor and adapt to network conditions seamlessly with useNetworkState.",
47 | id: "use-network-state",
48 | },
49 | {
50 | name: "useMediaQuery",
51 | description: "Track the state of a media query with useMediaQuery.",
52 | id: "use-media-query",
53 | },
54 | {
55 | name: "useOrientation",
56 | description: "Track the orientation of the device with useOrientation.",
57 | id: "use-orientation",
58 | },
59 | {
60 | name: "useSessionStorage",
61 | description: "Persist state to session storage with useSessionStorage.",
62 | id: "use-session-storage",
63 | },
64 | {
65 | name: "usePreferredLanguage",
66 | description:
67 | "Detect the user's preferred language with usePreferredLanguage.",
68 | id: "use-preferred-language",
69 | },
70 | {
71 | name: "useFetch",
72 | description: "Fetch data with ease and flexibility with useFetch.",
73 | id: "use-fetch",
74 | },
75 | {
76 | name: "useContinuousRetry",
77 | description:
78 | "Automates retries of a callback function until it succeeds with useContinuousRetry",
79 | id: "use-continuous-retry",
80 | },
81 | {
82 | name: "useVisibilityChange",
83 | description:
84 | "Track the visibility state of the document with useVisibilityChange.",
85 | id: "use-visibility-change",
86 | },
87 | {
88 | name: "useScript",
89 | description: "Load a script and track its state with useScript.",
90 | id: "use-script",
91 | },
92 | {
93 | name: "useRenderInfo",
94 | description: "Access information about the render with useRenderInfo.",
95 | id: "use-render-info",
96 | },
97 | ];
98 |
99 | export default function Index() {
100 | return (
101 |
102 |