Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[React 19] useOptimistic rolls back the state for no reason #31967

Closed
MrOxMasTer opened this issue Jan 3, 2025 · 8 comments
Closed

[React 19] useOptimistic rolls back the state for no reason #31967

MrOxMasTer opened this issue Jan 3, 2025 · 8 comments
Labels

Comments

@MrOxMasTer
Copy link

MrOxMasTer commented Jan 3, 2025

Summary

Tried all the ways server actions, optimistic updates and forms/buttons interact, but nothing. It just rolls back for no reason. No errors on the server, no errors inside the code either, but it still rolls back.

react.mp4--online-audio-convert.com.mov

Minimal example:
https://codesandbox.io/p/sandbox/modern-morning-cjxnnr

My code :

export const WarehousesTable = ({ warehouses, ...props }: WarehousesProps) => {
	// FIXME: https://github.com/facebook/react/issues/31967
	// const warehouses = use(promise);

	const [optimisticWarehouses, deleteOptimistic] = useOptimistic<
		Warehouse[],
		string
	>(
		[{ id: "sadasd", capacity: 100, location: "asdas", name: "sdasd" }],
		(prev, id) => {
			return prev.filter((w) => w.id !== id);
		},
	);

	const deleteAction = async (formData: FormData) => {
		const id = formData.get("id") as string;

		deleteOptimistic(id);

		await new Promise((resolve) => setTimeout(resolve, 1000));
		// await deleteWarehouseAction(formData, id);

		// throw new Error("SADsd");
	};

	// useEffect(() => {
	// 	console.log(isPending);
	// }, [isPending]);

	useEffect(() => {
		console.log("OPTIM: ", optimisticWarehouses);
	}, [optimisticWarehouses]);

	return (
		<TabsContent value="warehouses" {...props}>
			<Card>
				<CardHeader className="flex justify-between flex-row gap-8">
					<div>
						<CardTitle>Warehouse management</CardTitle>
						<CardDescription>
							View and editing information about warehouses
						</CardDescription>
					</div>
					<Button variant={"outline"} asChild>
						<Link href="/warehouses/add">Add</Link>
					</Button>
				</CardHeader>
				<CardContent>
					<Table>
						<TableCaption>List of active warehouses</TableCaption>
						<TableHeader>
							<TableRow>
								<TableHead>ID</TableHead>
								<TableHead>Name</TableHead>
								<TableHead>Location</TableHead>
								<TableHead>Capacity</TableHead>
								<TableHead>Actions</TableHead>
							</TableRow>
						</TableHeader>
						<TableBody>
							{optimisticWarehouses.map(({ id, name, capacity, location }) => {
								return (
									<TableRow key={id}>
										<TableCell>{id}</TableCell>
										<TableCell>{name}</TableCell>
										<TableCell>{capacity}</TableCell>
										<TableCell>{location}</TableCell>
										<TableCell>
											<div className="flex space-x-2">
												{/* FIXME: LINK /${id}/edit */}
												<Link href={`/warehouses/edit/${id}`}>
													<Button variant="outline" size="sm">
														<PenSquare className="h-4 w-4" />
													</Button>
												</Link>
												<form
													action={deleteAction}
													// onSubmit={async (e) => {
													// 	e.preventDefault();

													// 	const formData = new FormData(e.currentTarget);

													// 	await deleteAction(formData);
													// }}
												>
													<input type="hidden" name="id" defaultValue={id} />
													<Button
														// onClick={async (e) => {
														// 	startTransition(() =>
														// 	);

														// 	startTransition(async () =>
														// 		deleteWarehouseActionWithId(),
														// 	);
														// }}
														variant="outline"
														size="sm"
													>
														<Trash2 className="h-4 w-4" />
													</Button>
												</form>
											</div>
										</TableCell>
									</TableRow>
								);
							})}
						</TableBody>
					</Table>
				</CardContent>
			</Card>
		</TabsContent>
	);
};

probably all ways of creating this functionality have been tried.

  • action with closure/with formData
  • call of optimistic update startTransition and without
  • removing the deleteOptimistic function into the action itself or creating it inside a hook
  • initial state in useState
  • call action from button with startTranistion

None of this helps.

P.S. Also, errors are not displayed in the console at all if they occur inside useOptimistic - it's inconvenient and it's not clear where what happened. For example, in this situation

React 19.0.0
Next.js 15.1.3

@MrOxMasTer MrOxMasTer changed the title [React 19] [React 19] useOptimistic rolls back the state for no reason Jan 3, 2025
@ghost
Copy link

ghost commented Jan 3, 2025

I believe it rolls back on you because it has detected you are black and trans. Have you tried racemixing?

@MrOxMasTer
Copy link
Author

I believe it rolls back on you because it has detected you are black and trans. Have you tried racemixing?

Totally out of place. On all sides.

@ghost
Copy link

ghost commented Jan 3, 2025

I believe black and trans people are out of place. On all sides. Have you tried racemixing?

@MrOxMasTer
Copy link
Author

MrOxMasTer commented Jan 5, 2025

I've probably figured out what the problem is: useOptimistic doesn't act as a standalone state and needs to be backed up by useState, although it's strange that it's not written. It only acts as a mapping.
This is probably done for compatibility with other libraries like redux, zustand, react query, since they don't need to store useState separately. Their state is already stored somewhere (useState or useSyncExternalStore or useContext (+ useState)) and is updated when the developer calls them and they simply don't need a separate state.
Although it looks strange that I need to write the same function in two places, and for things using use() it doesn't fit and it needs a separate state.
I'll probably have to make custom hooks like useOptimisticState for simpler solutions, but that's okay.

Example:
https://codesandbox.io/p/sandbox/headless-grass-twtrdm?workspaceId=ws_P76u2mqnxpz53LD7675Jgf

@Mor10Mort
Copy link

Mor10Mort commented Jan 5, 2025

Thank you, MrOxMaster! 🙌
I initialized the state and passed it to useOptimistic, also updated it in formAction - it worked perfectly.
I also removed revalidatePath from my server action since I'm refetching the feed after a successful update.

@Eng1Mahmoud
Copy link

you can see this video to understand how useOptimistic work
https://youtu.be/VHI5ViqrF9g

note useOptimistic give you optimistic data to use it while action is pending and then you should update original state to keep show state in ui

@MrOxMasTer
Copy link
Author

you can see this video to understand how useOptimistic work https://youtu.be/VHI5ViqrF9g

note useOptimistic give you optimistic data to use it while action is pending and then you should update original state to keep show state in ui

it's not explained anywhere else exactly, but I've already figured that out. Thank you for confirming my hunch

@Eng1Mahmoud
Copy link

@MrOxMasTer ok also you can see example about useOptimistic on this repo 👇

https://github.com/Eng1Mahmoud/react-19

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants
@Mor10Mort @MrOxMasTer @Eng1Mahmoud and others