@next-box/link-extra 0.0.19 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Link.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import { type AnchorHTMLAttributes, type ReactNode } from 'react';
2
- import { addCacheBusterQueryParam, doUrlAndLocationMatch } from '#helpers.ts';
1
+ import { type AnchorHTMLAttributes, type JSX, type ReactNode } from 'react';
2
+ import { addCacheBusterQueryParam, shouldUrlAndLocationMatch } from '#helpers.ts';
3
3
  import { useLinkExtra } from './useLinkExtra.ts';
4
4
 
5
5
  export type LinkProps<T extends object> = {
@@ -19,24 +19,27 @@ export const Link = <T extends object>({
19
19
  prefetch = false,
20
20
  scroll = false,
21
21
  ...otherProps
22
- }: LinkProps<T>) => {
23
- const { NextLink, OwnLink, setLinkClicked } = useLinkExtra();
22
+ }: LinkProps<T>): JSX.Element => {
23
+ // This and the two other disables below are false positives
24
+ // eslint-disable-next-line @eslint-react/static-components
25
+ const { NextLink, OwnLink, onLinkClicked } = useLinkExtra();
24
26
 
25
27
  if (disableRouterCache) {
26
28
  href = addCacheBusterQueryParam(href);
27
29
  }
28
30
 
29
31
  return (
30
- // @ts-expect-error Next.js types wrong for nodenext
32
+ // This needs to remain: https://github.com/vercel/next.js/discussions/76329
33
+ // eslint-disable-next-line @typescript-eslint/no-deprecated, @eslint-react/static-components
31
34
  <NextLink href={href} legacyBehavior passHref prefetch={prefetch} scroll={scroll}>
32
- <OwnLink
35
+ <OwnLink // eslint-disable-line @eslint-react/static-components
33
36
  onClick={event => {
34
37
  if (onClick) {
35
38
  onClick(event);
36
39
  }
37
40
 
38
- if (!doUrlAndLocationMatch(href) && !event.defaultPrevented) {
39
- setLinkClicked(true);
41
+ if (!shouldUrlAndLocationMatch(href) && !event.defaultPrevented) {
42
+ onLinkClicked();
40
43
  }
41
44
  }}
42
45
  {...otherProps}
@@ -1,19 +1,19 @@
1
- import Link from 'next/link.js';
2
- import { type ComponentType, type ReactNode, createContext, useEffect, useState } from 'react';
3
- import { type LinkContextData, type OwnLinkProps } from '#types.js';
1
+ import type Link from 'next/link.js';
2
+ import {
3
+ type ComponentType,
4
+ type FC,
5
+ type ReactNode,
6
+ createContext,
7
+ useCallback,
8
+ useEffect,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
12
+ import { type LinkContextData, type NextLinkComponent, type OwnLinkProps } from '#types.js';
4
13
 
5
- // Needs to return explicit value
6
- // eslint-disable-next-line unicorn/no-useless-undefined
7
- const NoopComponent = () => undefined;
8
- // This is a noop
9
- // eslint-disable-next-line @typescript-eslint/no-empty-function
10
- const noop = () => {};
11
-
12
- export const LinkContext = createContext<LinkContextData>({
13
- NextLink: Link,
14
- OwnLink: NoopComponent,
15
- setLinkClicked: noop,
16
- });
14
+ // Context requires an initial value, but this is set in the provider so casting
15
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
16
+ export const LinkContext = createContext<LinkContextData>({} as LinkContextData);
17
17
 
18
18
  export type LinkProviderProps = {
19
19
  LoadingComponent: ComponentType;
@@ -24,55 +24,47 @@ export type LinkProviderProps = {
24
24
  pathname: string;
25
25
  };
26
26
 
27
- let timeoutId: NodeJS.Timeout | undefined;
28
-
29
- export const LinkProvider = ({
27
+ export const LinkProvider: FC<LinkProviderProps> = ({
30
28
  LoadingComponent,
31
29
  NextLink,
32
30
  OwnLink,
33
31
  children,
34
32
  loadingTimeout = 500,
35
33
  pathname,
36
- }: LinkProviderProps) => {
37
- const [linkClicked, setLinkClicked] = useState<boolean>(false);
34
+ }) => {
35
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
38
36
  const [showLoading, setShowLoading] = useState<boolean>(false);
39
37
 
40
- useEffect(() => {
41
- if (linkClicked) {
42
- timeoutId = setTimeout(() => {
43
- setShowLoading(true);
44
- }, loadingTimeout);
38
+ const onLinkClicked = useCallback(() => {
39
+ if (timeoutRef.current) {
40
+ clearTimeout(timeoutRef.current);
45
41
  }
46
42
 
47
- return () => {
48
- clearTimeout(timeoutId);
49
- };
50
- // We only want to re-execute when linkClicked changes
51
- // eslint-disable-next-line react-hooks/exhaustive-deps
52
- }, [linkClicked]);
43
+ timeoutRef.current = setTimeout(() => {
44
+ setShowLoading(true);
45
+ }, loadingTimeout);
46
+ }, [loadingTimeout]);
53
47
 
54
48
  useEffect(() => {
55
- if (linkClicked) {
56
- setLinkClicked(false);
57
- setShowLoading(false);
58
- globalThis.scroll(0, 0);
59
- }
60
-
61
- if (timeoutId) {
62
- clearTimeout(timeoutId);
49
+ if (timeoutRef.current) {
50
+ clearTimeout(timeoutRef.current);
63
51
  }
64
52
 
65
- // We only want to re-execute when pathname changes
66
- // eslint-disable-next-line react-hooks/exhaustive-deps
53
+ // This is setting state based on pathname change
54
+ // eslint-disable-next-line react-hooks/set-state-in-effect, @eslint-react/set-state-in-effect
55
+ setShowLoading(false);
56
+ scroll(0, 0);
67
57
  }, [pathname]);
68
58
 
69
59
  return (
70
- <LinkContext.Provider
60
+ <LinkContext
71
61
  value={{
72
- NextLink,
62
+ // Casting due to issues marrying up prop type and context type
63
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
64
+ NextLink: NextLink as unknown as NextLinkComponent,
73
65
  OwnLink,
74
- setLinkClicked: clicked => {
75
- setLinkClicked(clicked);
66
+ onLinkClicked: () => {
67
+ onLinkClicked();
76
68
  },
77
69
  }}
78
70
  >
@@ -80,6 +72,6 @@ export const LinkProvider = ({
80
72
  {children}
81
73
  {showLoading ? <LoadingComponent /> : undefined}
82
74
  </>
83
- </LinkContext.Provider>
75
+ </LinkContext>
84
76
  );
85
77
  };
package/src/helpers.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { nanoid } from 'nanoid';
2
2
 
3
- export const doUrlAndLocationMatch = (url: string) => {
3
+ export const shouldUrlAndLocationMatch = (url: string): boolean => {
4
4
  // It is possible for this to be undefined
5
5
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
6
6
  if (!globalThis.location) {
7
7
  return false;
8
8
  }
9
9
 
10
- const parsedUrl = URL.parse(url, globalThis.location.origin);
10
+ const parsedUrl = URL.parse(url, location.origin);
11
11
 
12
12
  if (!parsedUrl) {
13
13
  return false;
@@ -20,15 +20,13 @@ export const doUrlAndLocationMatch = (url: string) => {
20
20
  urlSearchParams.delete('y');
21
21
  }
22
22
 
23
- const locationSearchParams = new URLSearchParams(globalThis.location.search);
23
+ const locationSearchParams = new URLSearchParams(location.search);
24
24
 
25
25
  if (locationSearchParams.has('y')) {
26
26
  locationSearchParams.delete('y');
27
27
  }
28
28
 
29
- return (
30
- `${pathname}?${urlSearchParams.toString()}` === `${globalThis.location.pathname}?${locationSearchParams.toString()}`
31
- );
29
+ return `${pathname}?${urlSearchParams.toString()}` === `${location.pathname}?${locationSearchParams.toString()}`;
32
30
  };
33
31
 
34
32
  export const addCacheBusterQueryParam = (url: string): string => {
@@ -38,13 +36,13 @@ export const addCacheBusterQueryParam = (url: string): string => {
38
36
  return url;
39
37
  }
40
38
 
41
- const parsedUrl = URL.parse(url, globalThis.location.origin);
39
+ const parsedUrl = URL.parse(url, location.origin);
42
40
 
43
41
  if (!parsedUrl) {
44
42
  return url;
45
43
  }
46
44
 
47
- const locationSearchParams = new URLSearchParams(globalThis.location.search);
45
+ const locationSearchParams = new URLSearchParams(location.search);
48
46
  const locationCacheBusterSearchParam = locationSearchParams.get('y');
49
47
 
50
48
  if (locationCacheBusterSearchParam) {
@@ -54,7 +52,7 @@ export const addCacheBusterQueryParam = (url: string): string => {
54
52
  const { pathname, search } = parsedUrl;
55
53
  const searchParams = new URLSearchParams(search);
56
54
 
57
- if (`${pathname}?${search}` === `${globalThis.location.pathname}?${locationSearchParams.toString()}`) {
55
+ if (`${pathname}?${search}` === `${location.pathname}?${locationSearchParams.toString()}`) {
58
56
  searchParams.set('y', locationCacheBusterSearchParam ?? nanoid());
59
57
  } else {
60
58
  searchParams.set('y', nanoid());
package/src/types.ts CHANGED
@@ -1,12 +1,18 @@
1
- import type NextLink from 'next/link.js';
2
- import { type AnchorHTMLAttributes, type ComponentType } from 'react';
1
+ import { type LinkProps } from 'next/link.js';
2
+ import { type AnchorHTMLAttributes, type ComponentType, type ReactNode } from 'react';
3
3
 
4
4
  // This needs to be as generic as possible
5
5
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
6
  export type OwnLinkProps = Record<string, any> & AnchorHTMLAttributes<HTMLAnchorElement>;
7
7
 
8
+ export type NextLinkComponent = ComponentType<
9
+ LinkProps & {
10
+ children: ReactNode;
11
+ }
12
+ >;
13
+
8
14
  export type LinkContextData = {
9
- NextLink: typeof NextLink;
15
+ NextLink: NextLinkComponent;
10
16
  OwnLink: ComponentType<AnchorHTMLAttributes<HTMLAnchorElement>>;
11
- setLinkClicked: (clicked: true) => void;
17
+ onLinkClicked: () => void;
12
18
  };
@@ -1,7 +1,7 @@
1
- import { useContext } from 'react';
1
+ import { use } from 'react';
2
2
  import { type LinkContextData } from '#types.ts';
3
3
  import { LinkContext } from './LinkProvider.tsx';
4
4
 
5
5
  export const useLinkExtra = (): LinkContextData => {
6
- return useContext(LinkContext);
6
+ return use(LinkContext);
7
7
  };