You can use both, depending on how you want to do it
Gamestate probably makes more sense, since it will stay alive even if players disconnect
If you are using a platform such as Steam or EOS, then each player has a unique identifier you can use to identify each player - In blueprint you can get it using Get Unique Net Id
on the player state
If not, however, it can get a bit messier, since there is no unique id. You could use the player’s name (which you can get from the player state using GetPlayerName, and is automatically filled), but that might be problematic if players disconnect
If you’re only using blueprints, then you would need to have a replicated array of some struct that holds the unique identifier (net id or player name depending) and the score for that player
You can have that array in your gamestate, and then through OnRep you can update your UI on the clients. Unfortunately you’ll have to go through each array element and match the unique identifier to the one in each widget, and if ti matches you can update the widget’s information
If a unique identifier is found that doesn’t have a matching widget, you can create a new widget, and if there is a widget but its player identifier is not in the array you can remove the widget
Alternatively, when creating the widgets, you can assign them a player state instead, and your game state can keep a map of unique ids to player score, and when a score is updated through gameplay, it can also update a replicated score variable with OnRep on the player state. Then the player state can broadcast an event during the score’s OnRep that the widget can react to to update itself.
As for creating the widgets, depending on your game, you might want to have your player states broadcast an even on begin play/end play that other systems can bind to (for example your game state). That way, your clients can listen to that event to react to player joining/leaving (such as the UI adding/removing widgets), which should fix the inconsistent number of players on each client