I have done both having the UI controlled directly by the game state and with the UI completely separate and instead reading the game state (or using signals, etc). Both have their pros and cons.
Having the game directly control the UI is a faster way to get it going quickly, especially depending on how you do it. For example, if you have the player handle the UI for showing the score, lives, etc, it can be very quick to setup as the player likely has all these variables already, so whenever these variables are set you can just update the UI as well. However, it becomes harder to refactor, and it can be difficult to work with other UI elements outside of the main/individual-scene. This can be handled by writing cross-scene code, using signals, or other methods, but its generally kinda messy in my experience.
Have a completely separate UI scene has the advantage of being fairly easy to extend, animate, and otherwise built on top of, but it has the downside on ease-of-use and speed for accessing variables. I’ve found the best way to do this is through signals and passing the variables to update the UI elements through the signal itself, but its still not super easy in some situations. Another solution is to use exported NodePaths that point to the nodes that have the variables and access them directly, though this can be not as modular depending on how its programmed and what variables need.
All in all, I generally try to keep the UI mostly separate and just pass information to it, but I hop between the two depending on the project. You can also mix-and-match, which can be helpful as well. Using the example of the player, you could have the HUD part of the player scene, but have the pause menu, transitions, visual effects, overlays, and other UI elements separate.