Skip to content

Commit

Permalink
Unstable Vite: support for custom servers (#7682)
Browse files Browse the repository at this point in the history
  • Loading branch information
pcattori authored Oct 24, 2023
1 parent 53e9164 commit 9d516dd
Show file tree
Hide file tree
Showing 8 changed files with 553 additions and 15 deletions.
86 changes: 84 additions & 2 deletions docs/future/vite.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ toc: false
| Feature | Node | Deno | Cloudflare | Notes |
| ---------------------------- | ---- | ---- | ---------- | --------------------------------------------------------------------- |
| Built-in dev server |||| |
| Other servers (e.g. Express) | | || |
| Other servers (e.g. Express) | | || |
| HMR |||| |
| HDR |||| |
| MDX routes |||| [Supported with some deprecations.][supported-with-some-deprecations] |
Expand Down Expand Up @@ -494,10 +494,92 @@ If you want to reuse values across routes, stick them in their own non-route mod
export const myValue = "some value";
```

#### Adding and Removing Hooks
#### Changing Hooks

React Fast Refresh cannot track changes for a component when hooks are being added or removed from it, causing full reloads just for the next render. After the hooks have been updated, changes should result in hot updates again. For example, if you add [`useLoaderData`][use_loader_data] to your component, you may lose state local to that component for that render.

Additionally, if you are destructuring a hook's return value, React Fast Refresh will not be able to preserve state for the component if the destructured key is removed or renamed.
For example:

```tsx
export const loader = () => {
return json({ stuff: "some things" });
};

export default function Component() {
const { stuff } = useLoaderData<typeof loader>();
return (
<div>
<input />
<p>{stuff}</p>
</div>
);
}
```

If you change the key `stuff` to `things`:

```diff
export const loader = () => {
- return json({ stuff: "some things" })
+ return json({ things: "some things" })
}

export default Component() {
- let { stuff } = useLoaderData<typeof loader>()
+ let { things } = useLoaderData<typeof loader>()
return (
<div>
<input />
- <p>{stuff}</p>
+ <p>{things}</p>
</div>
)
}
```

then React Fast Refresh will not be able to preserve state `<input />` ❌.

As a workaround, you could refrain from destructuring and instead use the hook's return value directly:

```tsx
export const loader = () => {
return json({ stuff: "some things" });
};

export default function Component() {
const data = useLoaderData<typeof loader>();
return (
<div>
<input />
<p>{data.stuff}</p>
</div>
);
}
```

Now if you change the key `stuff` to `things`:

```diff
export const loader = () => {
- return json({ things: "some things" })
+ return json({ things: "some things" })
}

export default Component() {
let data = useLoaderData<typeof loader>()
return (
<div>
<input />
- <p>{data.stuff}</p>
+ <p>{data.things}</p>
</div>
)
}
```

then React Fast Refresh will preserve state for the `<input />`, though you may need to use [component keys](#component-keys) as described in the next section if the stateful element (e.g. `<input />`) is a sibling of the changed element.

#### Component Keys

In some cases, React cannot distinguish between existing components being changed and new components being added. [React needs `key`s][react_keys] to disambiguate these cases and track changes when sibling elements are modified.
Expand Down
87 changes: 87 additions & 0 deletions integration/helpers/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { spawn } from "node:child_process";
import type { Readable } from "node:stream";
import execa from "execa";
import getPort from "get-port";
import resolveBin from "resolve-bin";
import waitOn from "wait-on";

const isWindows = process.platform === "win32";

export async function viteDev(
projectDir: string,
options: { port?: number } = {}
) {
let viteBin = resolveBin.sync("vite");
return node(projectDir, [viteBin, "dev"], options);
}

export async function node(
projectDir: string,
command: string[],
options: { port?: number } = {}
) {
let nodeBin = process.argv[0];
let proc = spawn(nodeBin, command, {
cwd: projectDir,
env: process.env,
stdio: "pipe",
});
let devStdout = bufferize(proc.stdout);
let devStderr = bufferize(proc.stderr);

let port = options.port ?? (await getPort());
await waitOn({
resources: [`http://localhost:${port}/`],
timeout: 10000,
}).catch((err) => {
let stdout = devStdout();
let stderr = devStderr();
throw new Error(
[
err.message,
"",
"exit code: " + proc.exitCode,
"stdout: " + stdout ? `\n${stdout}\n` : "<empty>",
"stderr: " + stderr ? `\n${stderr}\n` : "<empty>",
].join("\n")
);
});

return { pid: proc.pid!, port: port };
}

export async function kill(pid: number) {
if (!isAlive(pid)) return;
if (isWindows) {
await execa("taskkill", ["/F", "/PID", pid.toString()]).catch((error) => {
// taskkill 128 -> the process is already dead
if (error.exitCode === 128) return;
if (/There is no running instance of the task./.test(error.message))
return;
console.warn(error.message);
});
return;
}
await execa("kill", ["-9", pid.toString()]).catch((error) => {
// process is already dead
if (/No such process/.test(error.message)) return;
console.warn(error.message);
});
}

// utils ------------------------------------------------------------

function bufferize(stream: Readable): () => string {
let buffer = "";
stream.on("data", (data) => (buffer += data.toString()));
return () => buffer;
}

function isAlive(pid: number) {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return false;
}
}
Loading

0 comments on commit 9d516dd

Please sign in to comment.