Skip to content

Best Practices ​

Guidelines for using PlayerState effectively on the server and client. Prefer short, focused patterns over large all-in-one handlers.

Quick tuning: Server performance Β· Client performance Β· Troubleshooting

Server ​

Validate before every write ​

  • Confirm the Player is in the game (player.Parent).
  • Validate types, ranges, and whitelists on your RemoteEvent / RemoteFunction handlers.
  • Never trust arguments from the client.
lua
function CurrencyService.AddCoins(player: Player, amount: number): boolean
    if not player or not player.Parent then
        return false
    end
    if typeof(amount) ~= "number" or amount <= 0 or amount > 1_000_000 then
        return false
    end
    return PlayerState.Increment(player, "Coins", amount)
end

Use the right lifecycle hook ​

WhenEvent
Final world β†’ profile write on leave onlyBeforeRelease
Also run on SaveData() during playBeforeSave
Analytics / cleanup after releaseProfileUnloaded (read-only)

See Server events.

Batch and increment ​

lua
PlayerState.SetValues(player, { Coins = c, Level = lvl, Experience = xp })
PlayerState.Increment(player, "Stats.Wins", 1)

Server authority ​

  • All persistent changes happen in server scripts.
  • Expose your own remotes; validate; then call PlayerState.
  • PlayerState does not ship clientβ†’server write remotes.

More: Server performance tips.


Client ​

Fallbacks on every read ​

  • Treat nil as β€œmissing,” not an error.
  • Default numbers with or 0, tables with or {} after a typeof check.
lua
local coins = PlayerState.Get("Coins") or 0

Wait for readiness only when it helps UX ​

  • Normal reads already wait for profile data.
  • Use IsReady() to show placeholders or defer heavy UIβ€”not to wrap every Get.

Narrow OnChanged subscriptions ​

  • Prefer "Coins" over ".".
  • Disconnect when UI or player context goes away.
lua
PlayerState.OnChanged("Coins", updateCoinsDisplay)

Read-only API ​

  • Client cannot call Set, SetPath, or save APIs.
  • Drive changes through server remotes you own.

More: Client performance tips Β· Error handling.


Shared (server + client) ​

Data shape ​

Keep paths shallow when you can

  • Deep nesting makes paths harder to validate and migrate.
  • Prefer CombatStats.SwordDamage over many nested tables.

Pick the right collection type

  • Arrays β€” ordered lists (inventory slots).
  • Dictionaries β€” keyed maps (settings by name).

Performance ​

AreaServerClient
Many fields at onceSetValues / batchN/A (read-only)
ListenersNarrow paths, disconnect on leaveNarrow paths, disconnect on UI destroy
Cache clearClearPathCache occasionallyClearCache for debug only

Memory management ​

Track connections per player or per UI and disconnect in PlayerRemoving or Destroying.

lua
local connections = {}

local function setupPlayer(player)
    connections[player] = {
        PlayerState.OnChanged("Coins", updateCoins),
        PlayerState.OnChanged("Level", updateLevel),
    }
end

local function cleanupPlayer(player)
    local conns = connections[player]
    if conns then
        for _, c in conns do
            if c then c:Disconnect() end
        end
        connections[player] = nil
    end
end

game.Players.PlayerRemoving:Connect(cleanupPlayer)

Security (server) ​

  • Whitelist setting names and value types.
  • Re-check prices and inventory on the server for every purchase.
lua
local validSettings = { MusicEnabled = "boolean", GraphicsQuality = "string" }
if typeof(value) ~= validSettings[settingName] then
    return false
end

Testing ​

  • Use separate DataStore name and Scope = "Testing" in development.
  • Provide mock PlayerData for Init(player, mockData) in test places.
lua
DataStore = {
    Name = isDev and "PlayerData_DEV" or "PlayerData_PROD",
    Scope = isDev and "Testing" or "Production",
}

Code organization ​

  • One service per domain: currency, inventory, settings.
  • Consistent names: getPlayerCoins, addPlayerCoins, CurrencyService.AddCoins.

Common pitfalls to avoid ​

Do not mutate data through a read result (server) ​

lua
-- Won't sync as intended
local inv = PlayerState.GetPath(player, "Inventory")
table.insert(inv, item)

-- Use API
PlayerState.AddToArray(player, "Inventory", item)

Table references from GetPath (server or client) ​

Returned tables may be live references. Clone before local edits, or write through SetPath / server APIs.

Do not assume data exists ​

lua
-- Risky
if PlayerState.Get("Coins") > 100 then

-- Safe
if (PlayerState.Get("Coins") or 0) > 100 then

Do not stack duplicate listeners ​

lua
-- Bad: 100 listeners
for i = 1, 100 do
    PlayerState.OnChanged("Coins", function() updateSlot(i) end)
end

-- Good: one listener
PlayerState.OnChanged("Coins", updateAllSlots)

PlayerState - High-Performance Roblox Data Management