Quickstart
A routed todo app: one module, one store, one controller, one view. Six files, and injection replaces prop drilling from the start.
1 · Install
npm install @solid-mvc/core solid-js
npm install -D vite-plugin-solid @babel/plugin-proposal-decorators @babel/plugin-transform-class-properties typescript viteDecorators need two tsconfig flags; JSX stays preserve so vite-plugin-solid compiles it:
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"jsx": "preserve",
"jsxImportSource": "solid-js"
}
}And vite-plugin-solid compiles JSX with @solid-mvc/core/web as the JSX runtime module — its createComponent is what turns View classes into host components:
import solidPlugin from 'vite-plugin-solid';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
solidPlugin({
solid: { moduleName: '@solid-mvc/core/web' },
babel: {
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-transform-class-properties', { loose: true }],
],
},
}),
],
});
useDefineForClassFields: falsematters:inject()field initializers must run inside the injection context the constructor opens — class-field semantics would evaluate them too early.
2 · A store
import { Injectable, signal, computed } from '@solid-mvc/core';
export interface Todo { id: number; text: string; done: boolean }
@Injectable()
export class TodoStore {
todos = signal<Todo[]>([]);
remaining = computed(() => this.todos.value.filter(t => !t.done).length);
add(text: string) {
this.todos.value = [
...this.todos.value,
{ id: Date.now(), text, done: false },
];
}
}3 · A controller
import { Controller, inject, signal } from '@solid-mvc/core';
import { TodoStore } from './todo.store';
@Controller()
export class TodoController {
private store = inject(TodoStore);
draft = signal('');
submit() {
const text = this.draft.value.trim();
if (text) this.store.add(text);
this.draft.value = '';
}
}4 · A view
import { For } from 'solid-js';
import { Component, View, inject } from '@solid-mvc/core';
import { TodoController } from './todo.controller';
import { TodoStore } from './todo.store';
@Component({ controllers: [TodoController] })
export class TodoView extends View {
private c = inject(TodoController);
private store = inject(TodoStore);
render() {
return (
<section>
<h2>Todos ({this.store.remaining.value} left)</h2>
<input
value={this.c.draft.value}
onInput={(e) => (this.c.draft.value = e.currentTarget.value)}
/>
<button onClick={() => this.c.submit()}>add</button>
<ul>
<For each={this.store.todos.value}>{(t) => <li>{t.text}</li>}</For>
</ul>
</section>
);
}
}5 · The module declares it all
import { Module } from '@solid-mvc/core';
import { TodoStore } from './todo.store';
import { TodoView } from './todo.view';
@Module({
providers: [TodoStore],
components: [TodoView],
bootstrap: [TodoView],
})
export class TodoModule {}6 · Bootstrap
import { bootstrap } from '@solid-mvc/core';
import { TodoModule } from './todo.module';
bootstrap(TodoModule, document.getElementById('app')!);What just happened
bootstrapcreated the app injector fromTodoModuleand mounted its bootstrap view inside it. JSX compiled against@solid-mvc/core/webroutes View classes throughcreateComponentinto host components — Solid never constructs them directly.- The view's injector resolved
TodoStore(declared on the module) and the declaredTodoController— once, at mount.render()runs once; signal reads inside JSX expressions are what Solid tracks.
Next: dependency injection for provider forms and scopes, or the router to add routes.