Best practices for syncing player data during animations on both clients?

I'm working on a turn based game where players can cast abilities at each other. For simplicity sake, just picture it as a card game. When building the online component I ran into an issue about when and how to save/sync data with the players. I'm curious to know how others have solved this.

The challenge was the following: Bob casts an ability which does 20 damage to Sally. As the animation ends it reduces the health of Sally, so the code says "Get the players health from the server, reduce by 20 and then save the data". Sally now needs to play the same animation, but is lagging behind Bob by a few seconds. Sally still needs to update their UI to the latest version so they say "Get my health value from the server, reduce it by 20 (without saving) and update the UI". This then creates a bug where instead of reducing their health from 100 to 80, their call to the server returned the already saved value of 80. When they applied the 20 damage they accidentally started from the value 80, which now displays the remaining health as 60.

The issue therefore becomes: When two players are playing the same animations at the same time, whoever saves the data first affects the server side data other players might need when animating second.

My way of working around this was to cache any value I need before the animations start. Bob and Sally receive a remote call that says "Effect X is going to trigger - backup all the data now". Bob runs his ability and saves the new data, so when Sally runs the ability animation it pulls the cached health value of 100, not the actual saved value of 80.

While this works well, it comes with a few annoyances.
  1. It can be fiddly backing up all the data before each ability is cast. It's easy to miss a reference to the cache and accidentally pull the latest value from the server
  2. It can still be a challenge to know when to actually save the new data. The abilities might effect other parts of the game that don't have any knowledge of the cached values, for example updating the player health might trigger a UI animation that always wants to start from the latest server value. It gets unwieldy trying to pass cached values around everywhere.

But I wonder how other games handle this. My game isn't that complex and while my caching solution works ok, it does seem like it would struggle to scale with large amounts of data or more than two players in a server.

One possibility is to perhaps treat each client as having a copy of all the game data at all times. In Photon, each player holds their own properties, which is uniquely segregated to them. PlayerB can access PlayerA properties, but they don't own that data. If PlayerA changes their data, PlayerB does not has a cached version that needs to be updated manually. But maybe that's how it's meant to work - if PlayerA changes their data, an event is sent out where each player updates and saves their own local copy of everything (so each Player.CustomProperties for each player contains all the information of every other player). Maybe that's a direction?

Does anyone have any experience in this area? If there are any articles/papers/github out there which covers a similar area, I'd really appreciate the link.