The library is designed to be general-purpose. To create your own reactive app, you only need to focus on changing the "Application Code" section. The Excalibur.js library code can be reused as-is.
First, decide what data your application needs. For example, to build a "Task List" app, you would define your own `initialState` object.
// 1. Define your application's initial state
const myInitialState = {
title: 'My To-Do List',
tasks: [
{ id: 1, text: 'Learn Excalibur.js', completed: true },
{ id: 2, text: 'Build a cool app', completed: false },
],
newTaskText: '' // To hold input from a text field
};
This step is the same. Pass your own state object to the `reactive()` function provided by the library.
// 2. Create a reactive store from your state
const store = reactive(myInitialState);
This is where your application comes to life. You connect your reactive data to the DOM using the `watchEffect` function. This function is the heart of the library's reactivity.
Think of `watchEffect` as a special box. You put a function inside it, and the library does two things:
Inside the `watchEffect`, you define your entire UI as an HTML string and set it using `innerHTML`. This is a simple but effective pattern:
watchEffect(() => {
// 1. Read from the store to build the HTML string.
// This automatically creates subscriptions.
appElement.innerHTML = `<h1>${playerStore.user.name}</h1>`;
// 2. The old HTML is gone, so we must re-attach event listeners.
document.getElementById('changeNameBtn').addEventListener('click', () => {
// 3. Modify the store. This triggers the effect to run again.
playerStore.user.name = 'New Name';
});
});
This creates a reactive loop: an action modifies the store, which triggers the effect to re-render the UI and re-attach the listeners, ready for the next action.
The design of `reactive()` and `watchEffect()` is heavily inspired by the Vue.js Composition API. This approach was chosen for several key reasons that make it excellent for beginners and experts alike:
You can copy the code below and use it in your own projects. It has no external dependencies.
// Excalibur.js - A mini reactivity library
let activeEffect = null;
const targetMap = new WeakMap();
function track(target, key) {
if (activeEffect) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(activeEffect);
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
const effectsToRun = new Set(dep);
effectsToRun.forEach(effect => effect());
}
}
function reactive(target) {
const handler = {
get(target, key, receiver) {
const isArray = Array.isArray(target);
if (isArray && key === 'length') {
track(target, 'length');
}
const value = Reflect.get(target, key, receiver);
track(target, key);
if (typeof value === 'object' && value !== null) {
return reactive(value);
}
return value;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const hadKey = Array.isArray(target) ? Number(key) < target.length : Object.prototype.hasOwnProperty.call(target, key);
const result = Reflect.set(target, key, value, receiver);
if (result && oldValue !== value) {
trigger(target, key);
if (Array.isArray(target) && (hadKey !== (Number(key) < target.length))) {
trigger(target, 'length');
}
}
return result;
}
};
return new Proxy(target, handler);
}
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}