Before you Continue
While the examples below omit interfaces when defining a crate's initial state for readability, it is always recommended to define interfaces for your crates before initializing them to keep your types explicit.
const crate = new Crate({
favoriteFoods: [],
})
crate.update({
favoriteFoods: (v) => [...v, "Apple Pie"] // ⚠️ Type error
})
interface User {
favoriteFoods: string[]
}
const crate = new Crate<User>({
favoriteFoods: [],
})
crate.update({
favoriteFoods: (v) => [...v, "Apple Pie"] // ✅ Works as expected.
})
Create
Create a new crate instance with a default state.
new Crate<T>(
defaultState: T,
) : Crate<T>
const playerCrate = new Crate({
coins: 0,
gems: 0,
xp: 0,
userSettings: {
music: true,
shadows: true,
}
});
Utility types
Crate exports helpful utility types to make the crate experience even sweeter.
This type allows you to extract the type of a crate's state.
const playerCrate = new Crate({
coins: 0,
gems: 0,
xp: 0,
userSettings: {
music: true,
shadows: true
}
})
type PlayerState = InferCrateType<typeof playerCrate>
const gemSelector = (state: PlayerState) => state.gems
I/O
API for updating and fetching data from the crate instance.
.update()
Update the state of the crate with a partial object.
Update Behavior
- All keys within the update object are optional.
- All values can either take a value of it's type, or a mutator function.
Crate.update(
dispatch: Dispatch<T>,
copy = false,
) : void
const playerCrate = new Crate({
coins: 0,
gems: 0,
xp: 0,
userSettings: {
music: true,
shadows: true
}
})
// Mutate both the health and walk speed.
Crate.update({
gems: (value) => value + 10, // Give the player 10 gems.
userSettings: {
music: false // Turn off the user's music.
shadows: (v) => !v // Toggle shadows.
}
})
As of v1.0.0
, the object passed into .update()
may be mutated by middleware.
This is intended behavior since cloning object literals is unnecessary, but know that it can cause problems when passing objects by reference.
To prevent this, crate provides a copy
parameter on the update method to automatically deep copy the provided table in these cases.
const defaultState = {
myVal: 0,
}
const crate = new Crate(defaultState)
crate.useMiddleware("myVal", (o, n) => n + 5)
// ❌ If we pass the object directly, the middleware **will** alter it.
crate.update(defaultState);
// ✅ Instead, let's tell crate to copy this object.
crate.update(defaultState, true);
.getState()
Get a value using a selector.
Crate.get(): T
const userCrate = new Crate({
coins: 0,
gems: 0,
xp: 0,
userSettings: {
music: true,
shadows: true
}
});
type UserType = InferCrateType<typeof userCrate>
userCrate.update({
userSettings: {
shadows: false,
}
});
// If you want to reuse this selector in the future:
const settingSelector = (state: UserType) => state.userSettings;
// Use the selector to access the state.
const settings = playerCrate.get(settingSelector, (settings) => print(settings)
print(settings)); // { music: true, shadows: false }
Listeners and Mutators
Crates have two types of listeners help you observe and mutate your state.
.useMiddleware()
You should never yeild under any circumstances within middleware.
The middleware api is dated and is very likely to change in the future.
middleware is invoked before state update to mutate the incoming state before it is dispatched.
Any value returned from it's callback will be pushed to the crate.
Crate.useMiddleware(
Key: keyof T,
Callback: (oldValue: T[U], newValue: T[U]) => T[U]
) : void
const playerState = new Crate({
health: 100,
maxHealth: 100,
});
// Simple middleware that prevents the health from going out of range.
playerState.useMiddleware("health", (oldValue, newValue) => {
return math.clamp(newValue, 0, playerState.get("maxHealth"));
});
.onUpdate()
onUpdate() is invoked after middleware, and only if the state has changed.
Updates only if with a given Key.
Crate.onUpdate<K>(
selector: (state: T) => K,
callback: (value: K) => RBXScriptConnection
) : void
const characterCrate = new Crate({
health: 100,
});
characterCrate.onUpdate((state) => state.health, (value) => {
print(value); // 20
});
characterCrate.update({
health: 20
});
.useDiff()
Use the diff object that is generated by the update method to detect changes. The resulting object only includes changed keys.
import { Crate } from "@rbxts/crate"
type TransactionID = string;
interface Transaction {
time: number,
assetID: number
}
interface PlayerData {
coins: number,
xp: number,
userSettings: {
music: boolean,
shadows: boolean,
},
meta: {
purchases: {
transactions: Record<TransactionID, Transaction>
}
}
}
const playerData = new Crate<PlayerData>({
coins: 0,
xp: 0,
userSettings: {
music: true,
shadows: true
},
meta: {
purchases: {
transactions: {}
}
}
})
playerData.useDiff((diff) => {
print(diff)
})
Let's send an update to the crate. We can see which values are expected to change.
playerData.update({
coins: 0, // No Changes
userSettings: {
music: false, // Change
shadows: true, // No Changes
},
meta: {
purchases: {
transactions: (v) => ({ // Change
...v,
a_transaction_uuid: {
assetID: 193915911,
time: 10,
}
}),
}
}
})
Given the above update method, We can expect the result from useDiff()
to look like this.
const diff = {
userSettings: {
music: false,
},
meta: {
purchases: {
transactions: {
a_transaction_uuid: {
assetID: 193915911,
time: 10,
}
}
}
}
}
Example: Networking
As of v1.0.0
, crate doesn't natively support networking, but useDiff()
can be used to efficiently replicate state to a client's crate.
Here is a very basic example using flamework networking.
export interface SharedCrateType {
serverMessage: string
}
export const sharedCrate = new Crate<SharedCrateType>({
serverMessage: "Hello, world!"
})
syncSharedCrate(diff: CrateDiff<SharedCrateType>): void;
sharedCrate.useDiff((diff) => {
Events.syncSharedCrate.broadcast(diff)
})
const serverMessage = sharedCrate.getState((s) => s.serverMessage)
print(serverMessage) // "Hello, world!"
sharedCrate.update({
serverMessage: "Loading..."
})
Events.syncSharedCrate.Connect((diff) => {
sharedCrate.update(diff)
const serverMessage = sharedCrate.getState((s) => s.serverMessage)
print(serverMessage) // "Loading..."
})
Execution Order
When state is changed, it goes through the pipleine in a specific order.
const Store = new Crate({
val: 10,
})
// 3. Finally, the onUpdate callbacks are invoked.
Store.onUpdate(
(state) => state.val,
(value) => {
print(value) // 21
}
)
// 2. Next, it goes through middleware to be mutated.
Store.useMiddleware("val", (oldValue, newValue) => {
return newValue + 1
})
// 1. First, the value is set
Store.update({
val: 20,
})
Utilities
.cleanup()
If you decide to stop using your crate instance for whatever reason, call .cleanup()
to ensure it's done properly.
This is final. Attemtping to call update()
after cleanup()
will cause an error.
myCrate.cleanup();
myCrate.update({}) // error!
static
reconcileDiff()
Crate exposes a static utility method for reconciling a received diff with existing data. This is only used when you want to integrate with an existing state system.
new Crate<T>(
state: T,
diff: CrateDiff<T>,
) : Crate<T>
// Client
Events.syncPlayerData.Connect(function(diff) => {
const reconciled = Crate.reconcileDiff(
clientStore.getState((state) => state.playerData),
diff
)
clientStore.setPlayerData(reconciled)
})