When you write a FiveM script that needs every player to agree on something (a car is locked, a player is on duty, the weather is rainy), there are two common tools: network events and state bags. Both work, but they solve different problems. Picking the wrong one is why some scripts show a car locked for one player and unlocked for another. This post compares the two with small Lua examples.
The quick difference
Events are messages. The server says "this just happened" and every client that receives it reacts. If a client wasn't connected, or the entity wasn't loaded on their side, they never hear about it.
State bags are stored values attached to something: an entity, a player or the whole server. Clients that come into range later still see the current value, because they read the state rather than waiting for a message.
A useful rule: if you are describing something that happened, use an event. If you are describing how something currently is, use a state bag.
The three kinds of state bag
-
Entity(entity).state: attached to a networked entity such as a vehicle, ped or object. Entity state bags rely on OneSync, which most roleplay servers run. -
Player(source).stateon the server,LocalPlayer.stateon the client: attached to a player. -
GlobalState: server-wide values that the server writes and every client can read.
Values should be simple data (strings, numbers, booleans and small tables). Functions can't be stored, and large tables that change often are a bad fit.
Example: vehicle locks with an event (the fragile way)
-- server
RegisterNetEvent('locks:toggle', function(netId)
TriggerClientEvent('locks:apply', -1, netId, true)
end)
-- client
RegisterNetEvent('locks:apply', function(netId, locked)
local veh = NetToVeh(netId)
if veh ~= 0 then
SetVehicleDoorsLocked(veh, locked and 2 or 1)
end
end)
This works until a player drives into the area after the event fired, or joins later. Their client never got the message, so the doors are open for them. Patching that with "request current state" callbacks means rebuilding state bags by hand.
Example: the same lock with a state bag
-- server
RegisterNetEvent('locks:toggle', function(netId)
local src = source
local veh = NetworkGetEntityFromNetworkId(netId)
if veh == 0 or not DoesEntityExist(veh) then return end
-- add your own distance and key/ownership check here
local locked = not Entity(veh).state.locked
Entity(veh).state:set('locked', locked, true)
end)
The third argument to set is replicated. With true, the value is sent to clients.
On the client, listen for changes with AddStateBagChangeHandler:
-- client
AddStateBagChangeHandler('locked', nil, function(bagName, key, value)
local veh = GetEntityFromStateBagName(bagName)
if veh == 0 then return end
SetVehicleDoorsLocked(veh, value and 2 or 1)
end)
Two details matter here:
- Use the
valueargument, notEntity(veh).state.locked. The handler runs before the new value is stored on the bag, so reading the bag inside the handler can give you the old value. -
GetEntityFromStateBagNamereturns0if the entity isn't loaded on this client yet. That's fine: when it does stream in, the state is already on it, and your script can readEntity(veh).state.lockedwhen the player tries to enter.
Example: player duty status
Duty status is a classic "how things currently are" value. Set it on the server when the job script clocks someone in:
-- server
local function setDuty(src, onDuty)
Player(src).state:set('onDuty', onDuty, true)
end
Any server script can now check it without asking the job resource:
-- server, in another resource
if not Player(src).state.onDuty then
return -- not clocked in, ignore the request
end
And the player's own client can read LocalPlayer.state.onDuty to show or hide job menus.
Example: server-wide values
Weather, time or "is the bank vault open" are good fits for GlobalState:
-- server
GlobalState:set('vaultOpen', false, true)
-- client
if GlobalState.vaultOpen then
-- show the vault interaction
end
When events are still the right choice
State bags don't replace events. Use events for:
- One-off actions: play a sound, show a notification, start an animation.
- Requests from a client to the server ("I want to lock this car"), which the server then validates.
- Anything that should not be repeated when a player streams in later, such as an explosion effect.
A good pattern combines both: the client sends an event as a request, the server validates it, and the server writes the result to a state bag.
Security: don't trust state you didn't write
Server-side checks should rely on values the server set itself. Depending on how your server is configured, clients may be able to write replicated values to their own player bag or to entities they control. If your server code reads a state bag value that a client could have written, treat it like any other client input and validate it.
Other good habits:
- Prefix keys with your resource name (
mylocks:locked) if you're worried about collisions with other scripts. - Don't write to a state bag every frame. Update it when the value actually changes.
- Keep values small. A boolean or a number is ideal; a full inventory table is not.
Where this shows up in real scripts
Vehicle scripts (locks, fuel, keys) and job scripts (duty, grade-based menus) are where state bags save the most bugs, so they're worth looking for when you review code. If you want readable examples, xFiveM Shop ships its resources with no escrow, so you can open its open source QBCore scripts and see how each one syncs data. If you're building lock or fuel features, its vehicle collection gives you test models to try them on.
Summary
Events describe things that happen, while state bags describe how things are. Use state bags for locks, duty status and server-wide flags so late joiners see the right values. Keep events for one-off actions and client requests, and always validate on the server before you write state.
How do you sync state in your resources? Share your approach in the comments.
Top comments (0)