Skip to content

Commit 56a406c

Browse files
bfangerBob Fanger
authored andcommitted
test: Added unittests for Link and NavLink from react-router v6
1 parent aab3977 commit 56a406c

19 files changed

+819
-87
lines changed

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ The preprocessor that is passed as an option is applied before running the prepr
9494
Once you've converted a React component to Svelte, you'd want delete that React component, but some if other React components depended on that component you can use `reactify` to use the new Svelte component as a React component.
9595

9696
```jsx
97-
import { reactify } from "resvelte-preprocess-react";
97+
import { reactify } from "svelte-preprocess-react";
9898
import ButtonSvelte from "../components/Button.svelte";
9999

100100
const Button = reactify(ButtonSvelte);
@@ -111,7 +111,15 @@ function MyComponent() {
111111
Using multiple frontend frameworks add overhead both in User and Developer experience.
112112

113113
- Increased download size
114-
- Slower (each framework boundry adds overhead)
114+
- Slower (each framework boundary adds overhead)
115115
- Context switching, keeping the intricacies of both Svelte and React in your head slows down development
116116

117-
svelte-preprocess-react is a migraton tool, it can be used to migrate _from_ or _to_ React, it's not a long term solution.
117+
svelte-preprocess-react is a migration tool, it can be used to migrate _from_ or _to_ React, it's not a long term solution.
118+
119+
# More info
120+
121+
- [reactify()](./docs/reactify.md) Convert a Svelte component into an React component
122+
- [hooks()](./docs/hooks.md) Using React hooks inside Svelte components
123+
- [useStore](./docs/useStore.md) Using a Svelte Store in a React components
124+
- [react-router](./docs/react-router.md) Migrate from react-router to SvelteKit
125+
- [Architecture](./docs/architecture.md) svelte-preprocess-react's API Design-principles and System architecture

docs/architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Architecture
22

3-
This document describes designdecisions and implementation details of the preprocessor.
3+
This document describes design-decisions and implementation details of the preprocessor.
44

55
**Principles:**
66

@@ -55,7 +55,7 @@ This array allows svelte-preprocess-react to inject the slotted content into the
5555

5656
### Server mode
5757

58-
Based off on how the Svelte component is compiled we can detect SSR and utitilize the renderToString method th generate the html. (limited to leaf nodes a.t.m.)
58+
Based off on how the Svelte component is compiled we can detect SSR and utilize the renderToString method th generate the html. (limited to leaf nodes a.t.m.)
5959

6060
This detection is done at runtime, so the client will also ship with the renderToStringYou server code.
6161
For smaller bundle size you can disable this feature by passing `ssr: false` to the preprocess function.

docs/hooks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# hooks
22

33
Using React hooks inside Svelte components.
4-
Because React doesn't have an synchronous render anymore (by-design), the initial value of the store will be `undefined`.
4+
Because React doesn't have a synchronous render (by-design), the initial value of the store will be `undefined`.
55

66
The `hooks()` function uses Svelte lifecycle functions, so you can only call the function during component initialization.
77

docs/react-router.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
1-
# ReactRouter
1+
# react-router
22

3-
Using multiple client side routers in an application is not recommended.
4-
Try to use the router of the framework you're migrating to.
3+
Using multiple routers in one app is a problem, as they are not built with that use-case in mind.
4+
To make migrate React components that using react-router to Svelte easier, we created `svelte-preprocess-react/react-router`
55

6-
svelte-preprocess-react provides limited compatibility ReactRouter.
6+
This is **not** a drop-in replacement for react-router, as it lacks many features, but it eases the migration process.
77

8-
This allows you to reuse your React components that use react-router-dom in with minimal changes.
8+
## What does it do?
9+
10+
It offers Hooks and Components that are used in the leaf nodes of the component tree, like:
11+
12+
- `<Link />`
13+
- `<NavLink />`
14+
- useLocation()
15+
- useHistory()
16+
- useParams()
17+
18+
This allows the basic things like reading info about the current url, rendering links and programmatic navigation to work.
19+
20+
## What it does NOT do?
21+
22+
Complex things like route matching, rendering routes, data-loading are out of scope.
23+
This becomes the job of the (svelte) router you're migrating to.
24+
25+
## How to use it?
926

1027
replace:
1128
`import { Link } from "react-router-dom"`
@@ -36,3 +53,5 @@ In src/routes/+layout.svelte
3653
<slot />
3754
</Router>
3855
```
56+
57+
As you can see the `<Router>` is exposing the push & replace actions but the actual navigation and url updates are done by the SvelteKit router.

docs/reactify.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@ const Dialog: React.FC<Props> = ({ onClose }) => (
2323
);
2424
```
2525

26-
React only has props, we we asume that the props starting with "on" followed by a capital letter are event handlers.
26+
React only has props, we we assume that the props starting with "on" followed by a capital letter are event handlers.

docs/useStore.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ In React and other regular javascript files this does _not_ work.
2424
To update the value and trigger an update use the `set` or `update` methods:
2525

2626
```ts
27-
// Instead of `$user = { name:'Jane Doe' }`
27+
// Instead of `$user = { name: "Jane Doe" }`
2828
user.set({ name: "Jane Doe" });
2929

30-
// Instead of `$user.name = 'Jane Doe'`
30+
// Instead of `$user.name = "Jane Doe"`
3131
user.update((user) => ({ ...user, name: "Jane Doe" }));
3232
```
3333

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"@sveltejs/adapter-static": "next",
5252
"@sveltejs/kit": "next",
5353
"@sveltejs/package": "^1.0.0-next.1",
54+
"@testing-library/react": "^13.4.0",
5455
"@testing-library/svelte": "^3.1.3",
5556
"@types/react": "^18.0.17",
5657
"@types/react-dom": "^18.0.6",
@@ -68,6 +69,7 @@
6869
"eslint-plugin-only-warn": "^1.0.3",
6970
"eslint-plugin-prettier": "^4.2.1",
7071
"eslint-plugin-svelte3": "^4.0.0",
72+
"happy-dom": "^7.6.6",
7173
"husky": "^8.0.1",
7274
"lint-staged": "^13.0.3",
7375
"postcss": "^8.4.16",

src/lib/react-router/Link.ts

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,48 @@
11
import * as React from "react";
2+
import locationToUrl from "./internal/locationToUrl.js";
3+
import RouterContext from "./internal/RouterContext.js";
4+
import type { To } from "./types";
25

3-
type Props = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
4-
component?: React.ComponentType<any> | undefined;
5-
to: string | Location | ((location: Location) => Location);
6+
export type LinkProps = Omit<
7+
React.AnchorHTMLAttributes<HTMLAnchorElement>,
8+
"href"
9+
> & {
610
replace?: boolean;
7-
innerRef?: React.Ref<HTMLAnchorElement> | undefined;
11+
to: To;
812
};
913

10-
const Link: React.FC<Props> = ({
11-
component = "a",
12-
to,
13-
replace,
14-
innerRef,
15-
children,
16-
...attrs
17-
}) => {
18-
const href = to ?? attrs.href;
19-
return React.createElement(
20-
component,
21-
{ ref: innerRef, ...attrs, href },
22-
children
23-
);
24-
};
14+
const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
15+
function LinkWithRef({ to, replace, children, ...rest }, ref) {
16+
const attrs = rest;
17+
const context = React.useContext(RouterContext);
18+
if (!context) {
19+
let pathname = "";
20+
if (typeof to === "string") {
21+
pathname = to;
22+
} else if (typeof to === "object") {
23+
pathname = to.pathname;
24+
}
25+
if (
26+
replace ||
27+
pathname.startsWith("/") === false ||
28+
/^[a+z]+:\/\//.test(pathname) === false
29+
) {
30+
// Without context only absolute paths are supported.
31+
throw new Error("Link was not wrapped inside a <Router>");
32+
}
33+
}
34+
const href = locationToUrl(to, context?.base).toString();
35+
if (replace) {
36+
const { onClick } = attrs;
37+
attrs.onClick = (event) => {
38+
onClick?.(event);
39+
if (!event.defaultPrevented) {
40+
event.preventDefault();
41+
context?.history.replace(href);
42+
}
43+
};
44+
}
45+
return React.createElement("a", { ...attrs, ref, href }, children);
46+
}
47+
);
2548
export default Link;

src/lib/react-router/NavLink.ts

Lines changed: 30 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,45 @@
11
import * as React from "react";
2-
import Link from "./Link.js";
3-
import useLocation from "./useLocation.js";
4-
import type { Location } from "./types";
2+
import Link, { type LinkProps } from "./Link.js";
3+
import useRouterContext from "./internal/useRouterContext.js";
4+
import locationToUrl from "./internal/locationToUrl.js";
5+
import type { RouteCondition } from "./types.js";
56

6-
type LinkProps = React.ComponentProps<typeof Link>;
7-
type Props = Omit<LinkProps, "className" | "style"> & {
8-
activeClassName?: string | undefined;
9-
activeStyle?: React.CSSProperties | undefined;
10-
exact?: boolean | undefined;
11-
strict?: boolean | undefined;
12-
isActive?(match: unknown, location: Location): boolean;
13-
location?: Location | undefined;
14-
className?: string | ((isActive: boolean) => string) | undefined;
7+
export type NavLinkProps = Omit<
8+
LinkProps,
9+
"className" | "style" | "children"
10+
> & {
11+
children?: React.ReactNode | ((condition: RouteCondition) => React.ReactNode);
12+
className?: string | ((condition: RouteCondition) => string | undefined);
1513
style?:
1614
| React.CSSProperties
17-
| ((isActive: boolean) => React.CSSProperties)
18-
| undefined;
19-
sensitive?: boolean | undefined;
15+
| ((condition: RouteCondition) => React.CSSProperties | undefined);
2016
};
21-
22-
const NavLink: React.FC<Props> = ({
23-
exact,
24-
strict,
25-
isActive,
26-
location,
27-
activeStyle,
28-
activeClassName,
17+
const NavLink: React.FC<NavLinkProps> = ({
2918
className,
3019
style,
3120
children,
3221
...rest
3322
}) => {
34-
const attrs: LinkProps = { ...rest };
35-
if (typeof attrs.to !== "string") {
36-
throw new Error("NavLink only supports string locations");
23+
const context = useRouterContext();
24+
const attrs: LinkProps = rest;
25+
const target = locationToUrl(attrs.to, context.base).toString();
26+
const current = locationToUrl(context.location, context.base).toString();
27+
const isActive = target === current;
28+
const condition: RouteCondition = { isActive };
29+
if (typeof className === "function") {
30+
attrs.className = className(condition);
31+
} else if (isActive) {
32+
attrs.className = className ? `${className} active` : "active";
33+
} else {
34+
attrs.className = className;
3735
}
3836

39-
const { pathname } = useLocation();
40-
const active = exact ? pathname === attrs.to : pathname.startsWith(attrs.to);
37+
attrs.style = typeof style === "function" ? style(condition) : style;
4138

42-
if (active) {
43-
if (activeClassName) {
44-
attrs.className = attrs.className
45-
? `${attrs.className} ${activeClassName}`
46-
: activeClassName;
47-
}
48-
if (activeStyle) {
49-
attrs.style = attrs.style
50-
? { ...attrs.style, ...activeStyle }
51-
: activeStyle;
52-
}
53-
}
54-
return React.createElement(Link, attrs, children);
39+
return React.createElement(
40+
Link,
41+
attrs,
42+
typeof children === "function" ? children(condition) : children
43+
);
5544
};
5645
export default NavLink;

src/lib/react-router/Router.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414

1515
<react:RouterContextProvider
1616
value={{
17+
base:
18+
typeof window === "undefined"
19+
? "http://localhost/"
20+
: window.location.href,
1721
location,
1822
params,
1923
history: {

0 commit comments

Comments
 (0)