T
TanStack3w ago
adverse-sapphire

Content flash with useOptimistic and router.invalidate

I'm trying to use useOptimistic to optimistically update some data. But when I call router.invalidate() (once the data has successfully been persisted), there is a brief flash. Minimal example in the thread.
2 Replies
adverse-sapphire
adverse-sapphireOP3w ago
Full reproducible example, just clone, npm install, npm run dev and go to localhost:3000. https://github.com/carderne/tanstack-start-use-optimistic/blob/main/src/routes/index.tsx Code that causes the issue:
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { appendFile, readFile } from "fs/promises";
import { startTransition, useOptimistic } from "react";

const DB = "./db.txt";

const getLinesFn = createServerFn({ method: "GET" }).handler(async () => {
try {
const lines = (await readFile(DB, "utf-8"))
.split("\n")
.filter((line) => line.length > 0);
return lines;
} catch (_) {
return [];
}
});

const writeLinesFn = createServerFn({ method: "POST" })
.validator((data: string) => data)
.handler(async ({ data }) => {
await appendFile(DB, data + "\n");
});

export const Route = createFileRoute("/")({
loader: () => getLinesFn(),
component: RouteComponent,
});

function RouteComponent() {
const lines = Route.useLoaderData();
const router = useRouter();
const [optimistic, addOptimistic] = useOptimistic(
lines,
(state, newItem: string) => {
return [...state, newItem];
},
);

const optimisticAction = async (formData: FormData) => {
const data = formData.get("content") as string;
addOptimistic(data);
startTransition(async () => {
await writeLinesFn({ data });
await router.invalidate({ sync: true });
});
};

return (
<div>
{optimistic.map((text, idx) => (
<div key={idx}>{text}</div>
))}
<form action={optimisticAction}>
<input name="content" />
<button>Add</button>
</form>
</div>
);
}
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { appendFile, readFile } from "fs/promises";
import { startTransition, useOptimistic } from "react";

const DB = "./db.txt";

const getLinesFn = createServerFn({ method: "GET" }).handler(async () => {
try {
const lines = (await readFile(DB, "utf-8"))
.split("\n")
.filter((line) => line.length > 0);
return lines;
} catch (_) {
return [];
}
});

const writeLinesFn = createServerFn({ method: "POST" })
.validator((data: string) => data)
.handler(async ({ data }) => {
await appendFile(DB, data + "\n");
});

export const Route = createFileRoute("/")({
loader: () => getLinesFn(),
component: RouteComponent,
});

function RouteComponent() {
const lines = Route.useLoaderData();
const router = useRouter();
const [optimistic, addOptimistic] = useOptimistic(
lines,
(state, newItem: string) => {
return [...state, newItem];
},
);

const optimisticAction = async (formData: FormData) => {
const data = formData.get("content") as string;
addOptimistic(data);
startTransition(async () => {
await writeLinesFn({ data });
await router.invalidate({ sync: true });
});
};

return (
<div>
{optimistic.map((text, idx) => (
<div key={idx}>{text}</div>
))}
<form action={optimisticAction}>
<input name="content" />
<button>Add</button>
</form>
</div>
);
}
Every time you add an item and hit enter, it immediately appears, then briefly flashes away while the loader reloads. The exact equivalent of this with Nextjs 15 does not have this issue (happy to share code if useful). Would be grateful if anyone can point out if I'm doing something wrong or if this is something inherent to Start/Router! I've tried with and without { sync: true }. The behaviour is slightly different: with sync, an extra item briefly appears and then disappears. Without it, the item you added disappears and then re-appears.
manual-pink
manual-pink3w ago
this is most likely caused by react not supporting transitions with external stores that router uses. they announced support for that in the future, so we'll see

Did you find this page helpful?