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
Playeris 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)
endUse the right lifecycle hook β
| When | Event |
|---|---|
| Final world β profile write on leave only | BeforeRelease |
Also run on during play | BeforeSave |
| Analytics / cleanup after release | ProfileUnloaded (read-only) |
See Server events.
Batch and increment β
- Multiple fields in one frame β
SetValuesor batch APIs. - Numeric changes β
Increment/Decrementinstead ofGet+Set.
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
nilas βmissing,β not an error. - Default numbers with
or 0, tables withor {}after atypeofcheck.
lua
local coins = PlayerState.Get("Coins") or 0Wait for readiness only when it helps UX β
- Normal reads already wait for profile data.
- Use
to show placeholders or defer heavy UIβnot to wrap everyIsReady()Get.
Narrow OnChanged subscriptions β
- Prefer
"Coins"over".". - Disconnect when UI or player context goes away.
lua
PlayerState.OnChanged("Coins", updateCoinsDisplay)Read-only API β
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.SwordDamageover many nested tables.
Pick the right collection type
- Arrays β ordered lists (inventory slots).
- Dictionaries β keyed maps (settings by name).
Performance β
| Area | Server | Client |
|---|---|---|
| Many fields at once | SetValues / batch | N/A (read-only) |
| Listeners | Narrow paths, disconnect on leave | Narrow paths, disconnect on UI destroy |
| Cache clear | ClearPathCache occasionally | ClearCache 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
endTesting β
- Use separate DataStore name and
Scope = "Testing"in development. - Provide mock
PlayerDataforin test places.Init(player, mockData)
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 thenDo 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)