The world of software development is changing rapidly. With the rise of powerful AI systems, more and more code is generated automatically, and the role of developers shifts toward designing architecture, defining rules and guiding the overall direction of a project. In this new reality our tools must evolve. React and React Native remain the backbone of modern frontend and mobile development, supported by a wide range of mature state management solutions such as Redux and Zustand. These libraries are flexible and powerful, but they intentionally leave many architectural decisions to individual teams. As projects grow and AI assisted development becomes more common, this flexibility can lead to highly divergent structures, making it harder to maintain consistency and predictability across large codebases.
AI operates best when the rules of the system are clear and consistent. The problem is that modern React applications rarely follow a common structure. Every team builds its own conventions, its own architecture and its own interpretation of how state should flow through the application. AI attempts to generate code for these environments but, without a shared standard, it often produces incorrect assumptions or invented patterns. The lack of predictable structure becomes a real barrier to reliable automation. Generated code can be inconsistent, difficult to review and hard to extend, and as the codebase grows, these inconsistencies accumulate, making maintenance and scaling significantly more challenging.
This is where a new approach is needed. I present a framework that offers a stable and unified architecture suitable both for developers and for AI assisted code generation. At its foundation is a global state model, a proven idea that already powers many established frameworks. A single, centralized state simplifies reasoning, eliminates scattered local stores and creates a clear map of how data moves through the system. Yet the most impactful part of the architecture comes next.
const defaultDB: DB = {
todos: new Map<TodoId, Todo>(), // an empty map of todos, keyed by id
showing: 'all', // show all todos
};
initAppDb(defaultDB);
One of the core concepts of this framework is how the state is updated. State changes are performed through "pure" functions that receive the current state and parameters and then produce a deterministic mutation of that state. These functions do not reach outside their scope, do not depend on external modules and do not perform side effects. While they are not pure in the strict functional sense, their behavior is fully constrained. Given the same inputs, they always produce the same change to the state. This makes the system easy to test. A state update can be verified by simply providing the initial state and parameters and comparing the result with the expected mutation. There are no hidden dependencies and no implicit behavior. Each update is a clear, isolated operation. For developers this means predictable code. For AI it means a contract that cannot be misinterpreted.
function addTodo ({ draftDb }, title: string) => {
const newTodo: Todo = {
id: now, // Simple ID generation
title: title.trim(),
done: false
};
draftDb.todos.set(newTodo.id, newTodo);
}
To eliminate dependency chains completely, these update functions are registered in the framework with unique identifiers. Instead of importing actions directly, they are invoked by ID. This removes the need for cross module dependencies and allows the framework to manage calls internally. AI or a developer can trigger any state change without knowing the file structure or package layout of the project. The result is a consistent architecture that prevents accidental divergence and keeps the entire codebase aligned.
regEvent(EVENT_IDS.ADD_TODO, addTodo);
In practice, this gives us a collection of self contained “pure” state update functions that can be organized in any way across packages or modules. Each function represents a single, well scoped change to the state, which makes the entire system easier to scale and maintain. These updates are straightforward to review, simple to reason about and trivial to test, because there are no implicit dependencies or side effects. As the application grows, this model keeps the codebase predictable, transparent and consistent for every team working on it.
At this point readers familiar with state management will naturally have questions. How are side effects handled? How does the UI stay efficient and responsive? How do we avoid unnecessary re renders if the entire application relies on a single global state?
These concerns are valid, and the framework addresses one of them through the next major component: an efficient reactive subscription system. Instead of making every field in the state reactive by default, the framework uses a reactive subscription graph. This design allows for a high level of precision and avoids the overhead that comes with broad, unscoped subscriptions. The process starts with the registration of root subscriptions. These are explicit entries that define which fields of the global state should be observed. A root subscription is triggered only when its corresponding field changes. This means that if the state contains hundreds of keys and only one of them mutates, only the relevant subscription reacts. No unnecessary computations, no wasted renders.
regSub(SUB_IDS.TODOS, 'todos');
On top of these root subscriptions you can build a graph of derived subscriptions. Each derived subscription depends on one or more root subscriptions or other derived ones. This forms a clean and predictable dependency graph. Every subscription is a pure function that selects or transforms the necessary data for the UI without performing side effects. Any subscription, root or derived, can be used directly in UI components. The result is a reactive layer that is explicit, granular and efficient. Developers have full control over what is reactive and why, and AI can rely on a deterministic structure that avoids accidental over subscriptions or unexpected recomputation.
regSub(SUB_IDS.ALL_COMPLETE, (todos: Todos) => {
const todosArray = Array.from(todos.values());
return todosArray.length > 0 && todosArray.every(todo => todo.done);
}, () => [[SUB_IDS.TODOS]]);
Subscriptions follow the same principles as state update functions. Each subscription is a pure function that receives input data from the reactive graph and returns exactly what the UI needs. Because subscriptions have no side effects and no external dependencies, they are straightforward to test in isolation. Just like update functions, subscriptions are registered in the framework by unique identifiers, which keeps them decoupled from file structure or module layout. Together, these two sets of simple, well scoped functions form a clean and scalable system. The architecture stays easy to reason about as it grows, since every piece of logic is atomic, deterministic and explicitly defined.
The next part of the framework addresses side effects, and it does so in an elegant and controlled way. Side effects are “dirty” functions that interact with the outside world, call APIs or communicate with other modules. They are registered in the framework by unique identifiers, just like state update functions and subscriptions. The key question is how to integrate side effects into this architecture without compromising its consistency and predictability. The solution is straightforward. A state update function can optionally return the identifier of a side effect along with the parameters it requires. The framework then triggers that effect after the state mutation completes. When the side effect finishes, its asynchronous result is dispatched into the system through another state update function that applies the necessary mutation based on the effect’s output. The state update function remains “pure” because it never performs the side effect itself and never receives it implicitly. The two concerns are fully separated: state logic stays deterministic, while external interactions remain isolated and explicit.
//events.ts
regEvent(EVENT_IDS.ADD_TODO, ({ draftDb }, title: string) => {
const newTodo: ....
draftDb.todos.set(newTodo.id, newTodo);
return [[EFFECT_IDS.TODOS_TO_LOCAL_STORE, current(draftDb.todos)]];
}
//effects.ts
regEffect(EFFECT_IDS.TODOS_TO_LOCAL_STORE, (todos) => {
const todosArray = Array.from(todos.entries());
localStorage.setItem(LS_KEY, JSON.stringify(todosArray));
});
At first glance, this model may feel unfamiliar, or even slightly uncomfortable. It differs from patterns commonly used in React applications today, and that contrast can be distracting at the beginning. However, the underlying concept is intentionally simple. Once the core rules become clear, the mental model settles quickly, and working within it becomes natural and consistent. Over time, the constraints stop feeling limiting and start to feel enabling. That said, this framework is designed to evolve. Feedback, experimentation and real world usage are essential to refining the model, and community involvement plays a central role in that process.
With that in mind, we can now move to the final part that completes the picture: the UI layer. This is where the entire data flow comes together and the architecture becomes tangible. State updates, subscriptions and events all converge here, forming a closed and predictable loop. The UI does not introduce new rules or special cases. It simply consumes subscriptions and dispatches state updates, keeping the full cycle explicit and easy to follow.
export const TodosExample: React.FC = () => {
const [title, setTitle] = useState('');
const todos = useSubscription<Todo[]>([SUB_IDS.VISIBLE_TODOS]);
const addTodo = () => {
const value = title.trim();
if (value.length > 0) {
dispatch([EVENT_IDS.ADD_TODO, value]);
setTitle('');
}
};
return (
<div>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addTodo()}
placeholder="New todo"
/>
<ul>
{todos.map(t => (
<li key={t.id}>{t.title}</li>
))}
</ul>
</div>
);
};
UI components remain intentionally simple. They do not contain business logic, data orchestration or implicit dependencies. A component subscribes to the data it needs and dispatches explicit state updates in response to user interactions. Nothing more. This keeps components focused, predictable and easy to reason about.
Because components depend only on subscriptions and dispatch functions, they are straightforward to test in isolation. Different application states can be simulated by registering alternative subscriptions, without mocking internal logic or global side effects. This also makes tools like Storybook a natural fit. Components can be rendered against predefined subscription outputs, allowing UI behavior to be explored, reviewed and validated independently of the rest of the system. As a result, UI code stays clean and approachable even as the application grows. Components remain independent, easy to understand and safe to change, reinforcing the overall goal of a scalable and maintainable architecture.
Before moving on to another important, and arguably the most important, part of the framework, the developer tools, it is worth clarifying what this framework actually is and where it stands today.
The short answer is that it is already usable in production. This framework is not an experimental prototype or a speculative idea. It is a direct adaptation of the ClojureScript framework re-frame, which has been developed, refined and battle tested for more than a decade. Re-frame has been used in large, long lived production systems across demanding domains, including finance and enterprise software, where correctness, predictability and maintainability are critical.
What is presented here is the same architectural model, reimplemented for the JavaScript and TypeScript ecosystem. The core ideas, constraints and data flow principles remain intact, while the implementation is designed to integrate naturally into modern JS and TS codebases. These concepts have already proven their reliability under real production conditions, which makes the framework suitable for production use today, not just experimentation.
The project is available publicly and can be explored here: https://reflex.js.org/
Now we can move to the more hands on part of the framework: the developer tools. Alongside debugging and inspection, these tools are designed to support a development workflow where AI plays an active role. The framework provides powerful DevTools that make the system observable at every level. State changes, subscriptions and events are all visible and traceable. This makes debugging straightforward and removes the guesswork that often comes with complex state driven applications. At the same time, the strict and explicit architecture allows AI to reason about the system, generate changes and verify behavior with far greater confidence.
To extend this idea further, Reflex also provides a dedicated DevTools MCP server. Through this interface, AI tools and autonomous agents can access structured information about the running application, including the current state, registered events and the full subscription graph. Instead of relying on static code analysis or incomplete assumptions, AI systems can query the architecture directly and operate on real runtime data. This allows agents to investigate issues, trace data flow and identify edge cases with much higher precision. By exposing the system through a stable protocol rather than internal APIs, the framework keeps developer workflows unchanged while enabling a deeper level of automation and observability for AI driven tooling.
Top comments (0)