SkillAgentSearch skills...

Charm

🍀 Atomic state management for Roblox

Install / Use

/learn @littensy/Charm
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<p align="center"> <p align="center"> <img width="150" height="150" src="https://raw.githubusercontent.com/littensy/charm/main/images/logo.png" alt="Logo"> </p> <h1 align="center"><b>Charm</b></h1> <p align="center"> Atomic state management for Roblox. <br /> <a href="https://npmjs.com/package/@rbxts/charm"><strong>npm package →</strong></a> </p> </p> <div align="center">

GitHub Workflow Status NPM Version GitHub License

</div>

Charm is an atomic and immutable state management library, inspired by Jotai and Nanostores. Store your state in atoms, and write your own functions to read, write, and observe state.

See an example of Charm's features in this example repository.

🍀 Features

  • ⚛️ Manage state with atoms. Decompose state into independent containers called atoms, as opposed to combining them into a single store.

  • 💪 Minimal, yet powerful. Less boilerplate — write simple functions to read from and write to state.

  • 🔬 Immediate updates. Listeners run asynchronously by default, avoiding the cascading effects of deferred updates and improving responsiveness.

  • 🦄 Like magic. Selector functions can be subscribed to as-is — with implicit dependency tracking, atoms are captured and memoized for you.


📦 Setup

Install Charm for roblox-ts using your package manager of choice.

npm install @rbxts/charm
yarn add @rbxts/charm
pnpm add @rbxts/charm

Alternatively, add littensy/charm to your wally.toml file.

[dependencies]
Charm = "littensy/charm@VERSION"

🐛 Debugging

Charm provides a debug mode to help you identify potential bugs in your project. To enable debug mode, set the global _G.__DEV__ flag to true before importing Charm.

Enabling __DEV__ adds a few helpful features:

  • Better error handling for selectors, subscriptions, and batched functions:

    • Errors provide the function's name and line number.
    • Yielding in certain functions will throw an error.
  • Server state is validated for remote event limitations before being passed to the client.

Enabling debug mode in unit tests, storybooks, and other development environments can help you catch potential issues early. However, remember to turn off debug mode in production to avoid the performance overhead.


📚 Reference

atom(state, options?)

Atoms are the building blocks of Charm. They are functions that hold a single value, and calling them can read or write to that value. Atoms, or any function that reads from atoms, can also be subscribed to.

Call atom to create a state container initialized with the value state.

local nameAtom = atom("John")
local todosAtom: Atom<{ string }> = atom({})

Parameters

  • state: The value to assign to the atom initially.

  • optional options: An object that configures the behavior of this atom.

    • optional equals: An equality function to determine whether the state has changed. By default, strict equality (==) is used.

Returns

The atom constructor returns an atom function with two possible operations:

  1. Read the state. Call the atom without arguments to get the current state.
  2. Set the state. Pass a new value or an updater function to change the state.
local function newTodo()
	nameAtom("Jane")

	todosAtom(function(todos)
		todos = table.clone(todos)
		table.insert(todos, "Buy milk")
		return todos
	end)

	print(nameAtom()) --> "Jane"
end

subscribe(callback, listener)

Call subscribe to listen for changes in an atom or selector function. When the function's result changes, subscriptions are immediately called with the new state and the previous state.

local nameAtom = atom("John")

local cleanup = subscribe(nameAtom, function(name, prevName)
	print(name)
end)

nameAtom("Jane") --> "Jane"

You may also pass a selector function that calls other atoms. The function will be memoized and only runs when its atom dependencies update.

local function getUppercase()
	return string.upper(nameAtom())
end

local cleanup = subscribe(getUppercase, function(name)
	print(name)
end)

nameAtom("Jane") --> "JANE"

Parameters

  • callback: The function to subscribe to. This may be an atom or a selector function that depends on an atom.

  • listener: The listener is called when the result of the callback changes. It receives the new state and the previous state as arguments.

Returns

subscribe returns a cleanup function.


effect(callback)

Call effect to track state changes in all atoms read within the callback. The callback will run once to retrieve its dependencies, and then again whenever they change. Your callback may return a cleanup function to run when the effect is removed or about to re-run.

local nameAtom = atom("John")

local cleanup = effect(function()
	print(nameAtom())
	return function()
		print("Cleanup function called!")
	end
end)

Because effect implicitly tracks all atoms read within the callback, it might be useful to exclude atoms that should not trigger a re-run. You can use peek to read from atoms without tracking them as dependencies.

Parameters

  • callback: The function to track for state changes. The callback will run once to retrieve its dependencies, and then again whenever they change.

Returns

effect returns a cleanup function.

Caveats

  • If your effect should disconnect itself, use the cleanup argument. Because effects run immediately, your effect may run before a cleanup function is returned. To disconnect an effect from the inside, use the argument passed to your effect instead:
    effect(function(cleanup)
    	if condition() then
    		cleanup()
    	end
    end)
    

computed(callback, options?)

Call computed when you want to derive a new atom from one or more atoms. The callback will be memoized, meaning that subsequent calls to the atom return a cached value that is only re-calculated when the dependencies change.

local todosAtom: Atom<{ string }> = atom({})
local mapToUppercase = computed(function()
	local result = table.clone(todosAtom())
	for key, todo in result do
		result[key] = string.upper(todo)
	end
	return result
end)

Because computed implicitly tracks all atoms read within the callback, it might be useful to exclude atoms that should not trigger a re-run. You can use peek to read from atoms without tracking them as dependencies.

This function is also useful for optimizing effect calls that depend on multiple atoms. For instance, if an effect derives some value from two atoms, it will run twice if both atoms change at the same time. Using computed can group these dependencies together and avoid re-running side effects.

Parameters

  • callback: A function that returns a new value depending on one or more atoms.

  • optional options: An object that configures the behavior of this atom.

Returns

computed returns a read-only atom.


observe(callback, factory)

Call observe to create an instance of factory for each key present in a dictionary or array. Your factory can return a cleanup function to run when the key is removed or the observer is cleaned up.

[!NOTE] Because observe tracks the lifetime of each key in your data, your keys must be unique and unchanging. If your data is not keyed by unique and stable identifiers, consider using mapped to transform it into a keyed object before passing it to observe.

local todosAtom: Atom<{ [string]: Todo }> = atom({})

local cleanup = observe(todosAtom, function(todo, key)
	print(`Added {key}: {todo.name}`)
	return function()
		print(`Removed {key}`)
	end
end)

Parameters

  • callback: An atom or selector function that returns a dictionary or an array of values. When a key is added to the state, the factory will be called with the new key and its initial value.

  • factory: A function that will be called whenever a key is added or removed from the atom's state. The callback will receive the key and the entry's initial value as arguments, and may return a cleanup function.

Returns

observe returns a cleanup function.


mapped(callback, mapper)

Call mapped to transform the keys and values of your state. The mapper function will be called for each key-value pair in the atom's state, and the new keys and atoms will be stored in a new atom.

local todosAtom: Atom<{ Todo }> = atom({})
local todosById = mapped(todosAtom, function(todo, index)
	return todo, todo.id
end)

Parameters

  • callback: The function whose result you want to map over. This can be an atom or a selector function that reads from atoms.

  • mapper: The mapper is called for each key in your state. Given the current value and key, it should return a new corresponding value and key:

    1. Return a single value to map the table's original key to a new value.
    2. Return two values, the first being the value and the second being the key, to update both keys and values.
    3. Return nil for the value to remove the key from the resulting table.

Returns

mapped returns a read-only atom.


peek(value, ...)

Call peek to call a function without tracking

Related Skills

View on GitHub
GitHub Stars197
CategoryDevelopment
Updated12h ago
Forks17

Languages

Luau

Security Score

100/100

Audited on Apr 6, 2026

No findings