62. Framework Docs
Merge State Management
Merge is Braid's state management library (braid-lang/lib/frameworks/merge/merge.br, 311 lines), inspired by Redux. It provides a centralized store with dispatch, subscriptions, computed values, undo/redo, selector caching, devtools integration, and a middleware pipeline.
Creating a Store
import merge;
let initialState = { count: 0, todos: [] };
let reducer = fn(state, action) {
if action.type == "INCREMENT" {
return { count: state.count + 1, todos: state.todos };
}
if action.type == "DECREMENT" {
return { count: state.count - 1, todos: state.todos };
}
if action.type == "SET_COUNT" {
return { count: action.payload, todos: state.todos };
}
if action.type == "ADD_TODO" {
return { count: state.count, todos: state.todos + [action.payload] };
}
return state;
};
let store = merge.merge_createStore(reducer, initialState);Core API
Dispatch & Get State
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "SET_COUNT", payload: 42 });
let current = store.getState();
// { count: 42, todos: [] }
// Standalone dispatch helper
merge.merge_dispatch(store, { type: "INCREMENT" });
merge.merge_getState(store);Subscribe to Changes
let unsubscribe = store.subscribe(fn(new_state, prev_state) {
print("State changed:", new_state);
});
// Later:
unsubscribe(); // removes the listener
// Standalone helper
let unsub = merge.merge_subscribe(store, fn(s, p) { ... });Computed Values (Selectors)
store.addComputed("doubleCount", ["count"], fn(state) {
return state.count * 2;
});
store.addComputed("pendingTodos", ["todos"], fn(state) {
return state.todos.filter(fn(t) { return !t.completed; });
});
let doubled = store.getComputed("doubleCount"); // 84
let pending = store.getComputed("pendingTodos");
// Standalone helpers
merge.merge_addComputed(store, "total", ["count"], fn(s) { return s.count; });
merge.merge_getComputed(store, "total");Selector Caching
let result = store.select(fn(state) {
return state.todos.filter(fn(t) { return t.completed; });
});
// Caches by selector function identity. Only recomputes when state changes.
// Access via merge.merge_select(store, selector)Middleware
Middleware are functions that intercept dispatched actions before they reach the reducer. They can modify, delay, or cancel actions.
// Create a store with middleware
let store = merge.merge_createStore(reducer, initialState, [myMiddleware]);
// Or apply middleware after creation
merge.apply_middleware(store, [mw1, mw2]);Built-in Middleware Factories
Logger Middleware
let logger = merge.create_logger_middleware();
// Logs every action and state transitionPersist Middleware
let storage = {
set: fn(key, val) { window.localStorage.setItem(key, val); }
};
let persist = merge.create_persist_middleware(storage, "my-app-state");
// Automatically persists state to storage after every action (except @@merge/REPLACE)Thunk Middleware (Async Actions)
let thunk = merge.create_thunk_middleware();
store = merge.merge_createStore(reducer, initialState, [thunk]);
// Now actions can be functions:
fn fetchUser(id) {
return fn(dispatch, getState) {
dispatch({ type: "FETCH_USER_START" });
let user = api.getUser(id);
dispatch({ type: "FETCH_USER_DONE", payload: user });
};
}
store.dispatch(fetchUser(42));Custom Middleware
let myMiddleware = merge.create_middleware(fn(store, action) {
print("Action:", action.type);
if action.type == "SECRET" {
return 0; // cancel the action (return 0 / nil)
}
return action; // pass through
});Undo / Redo
Merge stores history automatically — up to the last 50 states.
store.undo(); // Revert to previous state
store.redo(); // Re-apply reverted state
store.canUndo() -> bool // true if undo is available
store.canRedo() -> bool
// Standalone helpers
merge.merge_undo(store);
merge.merge_redo(store);
// History management
store.getHistory() -> list // Full history array
store.jumpToHistory(index) // Jump to specific pointDevtools Integration
let devtools = merge.create_devtools_hook(); // Base implementation
// Or connect to browser devtools:
let custom_hook = {
init: fn(state) { /* send initial state */ },
send: fn(action, state) { /* log action + state */ },
subscribe: fn(callback) { /* listen for time-travel */ }
};
store.connectDevtools(custom_hook);
// Each dispatch automatically calls devtools.send(action, state)
merge.merge_connectDevtools(store, custom_hook);Replace Reducer
store.replaceReducer(fn(state, action) {
// New reducer logic
});
// Dispatches @@merge/REPLACE internallyCounter Example
import bond;
import merge;
let counterReducer = fn(state, action) {
if action.type == "INCREMENT" { return { count: state.count + 1 }; }
if action.type == "DECREMENT" { return { count: state.count - 1 }; }
if action.type == "RESET" { return { count: 0 }; }
return state;
};
let store = merge.merge_createStore(counterReducer, { count: 0 });
store.addComputed("isPositive", ["count"], fn(s) {
return s.count >= 0;
});
fn Counter() {
return bond.div({ class: "counter" }, [
bond.h1("Count: " + string(store.getState().count), {}),
bond.button("+", { onClick: fn() { store.dispatch({ type: "INCREMENT" }); } }),
bond.button("-", { onClick: fn() { store.dispatch({ type: "DECREMENT" }); } }),
bond.button("Reset", { onClick: fn() { store.dispatch({ type: "RESET" }); } }),
bond.p("Positive: " + string(store.getComputed("isPositive")), {})
]);
}Todo List with Middleware
import bond;
import merge;
let todoReducer = fn(state, action) {
if action.type == "ADD_TODO" {
return { todos: state.todos + [{
id: state.todos.length + 1,
text: action.payload,
completed: false
}]};
}
if action.type == "TOGGLE_TODO" {
let new_todos = [];
let i = 0;
while i < state.todos.length {
let t = state.todos[i];
if t.id == action.payload {
t.completed = !t.completed;
}
new_todos.push(t);
i = i + 1;
}
return { todos: new_todos };
}
if action.type == "REMOVE_TODO" {
let filtered = [];
let i = 0;
while i < state.todos.length {
if state.todos[i].id != action.payload {
filtered.push(state.todos[i]);
}
i = i + 1;
}
return { todos: filtered };
}
return state;
};
let logger = merge.create_logger_middleware();
let thunk = merge.create_thunk_middleware();
let store = merge.merge_createStore(todoReducer, { todos: [] }, [logger, thunk]);
store.addComputed("activeCount", ["todos"], fn(s) {
let count = 0;
let i = 0;
while i < s.todos.length {
if !s.todos[i].completed { count = count + 1; }
i = i + 1;
}
return count;
});
store.addComputed("completedCount", ["todos"], fn(s) {
let count = 0;
let i = 0;
while i < s.todos.length {
if s.todos[i].completed { count = count + 1; }
i = i + 1;
}
return count;
});
// Async action via thunk
fn addTodoAsync(text) {
return fn(dispatch, _getState) {
dispatch({ type: "ADD_TODO", payload: text });
};
}
store.dispatch(addTodoAsync("Learn Braid Merge"));
store.dispatch({ type: "TOGGLE_TODO", payload: 1 });
print("Active:", store.getComputed("activeCount"));
print("Completed:", store.getComputed("completedCount"));
// Undo the last action
store.undo();Async Actions without Thunk
Merge also supports async actions natively via the async_type field:
store.dispatch({
async_type: "FETCH_USER", // dispatches FETCH_USER_RESOLVED on completion
payload: fn(dispatch, getState) {
return api.getUser();
}
});
// Automatically dispatches { type: "FETCH_USER_RESOLVED", payload: <result> }Store Internals
// The store object contains:
{
state, // current state
listeners, // subscriber functions
middleware, // middleware pipeline
reducer, // reducer function
computed, // computed values cache
computed_deps, // dependency map for computed values
compute_fns, // compute functions
history, // state history (max 50)
history_index, // current position in history
devtools, // devtools hook
selector_cache, // cache for select()
dispatch, // fn(action) -> void
getState, // fn() -> state
subscribe, // fn(listener) -> unsubscribe
undo, // fn() -> void
redo, // fn() -> void
select, // fn(selector) -> result
addComputed, // fn(name, deps, fn)
getComputed, // fn(name) -> value
connectDevtools, // fn(devtools_hook)
replaceReducer, // fn(new_reducer)
...
}