Using only events to keep contract state sync on the client is a good practice?

As you know, the efficient way to store complex data in contracts is usually not the best way to get it back to the user.

Suppose I need a mapping that stores allowed addresses, but I can also remove them:

mapping(address => bool) private allowedAddresses;

// Add new address:
allowedAddresses[address] = true;

// Remove an addresses
allowedAddresses[address] = false

Although it is efficient to write, delete and check if a specific address is valid, I cannot return the list of valid addresses.

If I used an array instead, I would have to reorganize the array every time I remove an item, which in addition to making the code more complex, adds considerable gas cost.

The conclusion I've come to is that in these cases it doesn't make sense to have reading functions of type "getAll" in the contract, it would make more sense for the contract state to be synchronized on the client from the events, in the same way that we synchronize the main network.

The downside of this is that we would have to cross-reference the different add and remove events and the greater the number of events, the more resources we consume from the client.

After all, there is already a debate about this, who knows even a good recommended practice?

I recently asked for opinions about this on Twitter, and there doesn't seem to be a generally agreed upon best practice. I think most people agree that eventually we want to be able to rely on events only, but that if you want to do that currently you need to introduce dependencies on off-chain indexers like The Graph which are a centralized component or if decentralized don't have great UX yet.