T
TanStack4mo ago
helpful-purple

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
helpful-purple
helpful-purpleOP4mo 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.
rare-sapphire
rare-sapphire4mo 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?