Documentation
📦 Crate
API Reference

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.

❌ Implicit Crate Type
const crate = new Crate({
    favoriteFoods: [],
})
 
crate.update({
	favoriteFoods: (v) => [...v, "Apple Pie"] // ⚠️ Type error
})
✅ Explicit Crate Type
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.

Type
new Crate<T>(
    defaultState: T,
) : Crate<T>
Usage
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.
Type
Crate.update(
    dispatch: Dispatch<T>,
    copy = false,
) : void
Usage
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.

Example
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.

Type
Crate.get(): T
Usage
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.

Type
Crate.useMiddleware(
    Key: keyof T,
    Callback: (oldValue: T[U], newValue: T[U]) => T[U]
) : void
Usage
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.

Type With Key
Crate.onUpdate<K>(
    selector: (state: T) => K,
    callback: (value: K) => RBXScriptConnection
) : void
Usage With Key
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.

Usage
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.

Update
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.

useDiff() Result
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.

Shared.ts
export interface SharedCrateType {
    serverMessage: string
}
 
export const sharedCrate = new Crate<SharedCrateType>({
    serverMessage: "Hello, world!"
})
Flamework Event Type
syncSharedCrate(diff: CrateDiff<SharedCrateType>): void;
Server.ts
sharedCrate.useDiff((diff) => {
    Events.syncSharedCrate.broadcast(diff)
})
 
const serverMessage = sharedCrate.getState((s) => s.serverMessage) 
print(serverMessage) // "Hello, world!"
 
sharedCrate.update({
    serverMessage: "Loading..."
})
Client.ts
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.

ExecutionModel.ts
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.

Usage
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.

Type
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)
})