Project structure
How a ShadowJS app is laid out on disk, what each file does, and which package provides which part of the framework.
A ShadowJS app is an ordinary Vite project. The framework adds one plugin, one TypeScript setting and two runtime packages. There is no required folder layout, router convention or config file of its own.
The files every app has#
my-app/
index.html page shell: <div id="app"> and the module script
package.json
tsconfig.json strict TypeScript, jsx = preserve, jsxImportSource = @shadow-js/web
vite.config.ts plugins: [shadow()]
src/
main.tsx entry point: render(() => <App />, #app)
App.tsx the root component
index.css
vite-env.d.ts /// <reference types="vite/client" />
| File | Role |
|---|---|
index.html |
Vite's entry. Contains the mount element and <script type="module" src="/src/main.tsx">. |
src/main.tsx |
The only place that touches the DOM directly. Finds the container and calls render (or hydrate for server-rendered HTML). |
src/App.tsx |
Your root component. Everything else is imported from here. |
vite.config.ts |
Registers the ShadowJS plugin (options). |
tsconfig.json |
Only type-checks; Vite and the plugin do the compiling (settings). |
npm create shadow-app writes exactly these files; see Quick start.
A layout that scales#
Nothing below is enforced. It is the structure the repository's own todomvc example uses, which keeps state out
of markup and gives each part a clear responsibility:
src/
main.tsx
App.tsx
components/ presentational and container components (one per file)
state/ hooks that own stores: useCounter, useTodos, ...
model/ plain types and pure functions (no framework imports)
storage/ persistence, network access
The example below follows that split. The hook owns the state and the operations, the component only renders,
App connects them and main.tsx mounts.
import { useStore } from "@shadow-js/core";
export function useCounter(start = 0) {
const [count, setCount] = useStore(start);
return {
count,
increment: () => setCount((n) => n + 1),
reset: () => setCount(start),
};
}
export interface CounterViewProps {
count: number;
onIncrement: () => void;
onReset: () => void;
}
export function CounterView(props: CounterViewProps) {
return (
<div>
<p>Count: {props.count}</p>
<button type="button" onClick={() => props.onIncrement()}>
Increment
</button>
<button type="button" onClick={() => props.onReset()}>
Reset
</button>
</div>
);
}
import { CounterView } from "./components/CounterView";
import { useCounter } from "./state/use-counter";
export function App() {
const counter = useCounter();
return (
<CounterView
count={counter.count()}
onIncrement={counter.increment}
onReset={counter.reset}
/>
);
}
import { render } from "@shadow-js/web";
import { App } from "./App";
const root = document.getElementById("app");
if (root === null) throw new Error("Missing #app element");
render(() => <App />, root);
Note count={counter.count()}: the prop is compiled to a getter, so CounterView re-reads it when the count
changes and CounterView's function body still runs once. See the mental model.
With server rendering#
A server-rendered app splits the entry in two and keeps App shared. The tutorial builds this in
step 8.
src/
App.tsx shared
entry-client.tsx hydrate(() => <App />, #app)
entry-server.tsx renderToString(() => <App />) (from @shadow-js/web/server)
Where each package fits#
| Package | Install as | You import | Provides |
|---|---|---|---|
@shadow-js/core |
dependency | useStore, useMemo, useEffect, onMount, onCleanup, createContext, useResource, batch, untrack, ... |
The reactive engine: signals, stores, memos, effects, owners, lists, resources. No DOM. |
@shadow-js/core/extend |
(same package) | createSignalNode, createComputation, createOwner, ... |
The extension API the built-ins are written against; for library authors (reference). |
@shadow-js/web |
dependency | render, hydrate, Show, For, Index, Switch, Match, Suspense, ErrorBoundary, lazy, Portal, Dynamic, Title, Meta, ... |
The DOM runtime, control-flow components, head management and the JSX types (@shadow-js/web/jsx-runtime). |
@shadow-js/web/server |
(same package) | renderToString, renderToStringAsync, renderToStream, serialize |
The server renderer and the helpers the server-target output calls. |
@shadow-js/vite |
dev dependency | shadow (default export) |
The Vite plugin that runs the compiler. |
@shadow-js/compiler |
installed by the plugin | compile |
The JSX compiler (DOM and server targets). Direct use is for tooling. |
@shadow-js/testing |
dev dependency | @shadow-js/testing/dom: render, userEvent, fireEvent, waitFor; renderHook, flush, captureWarnings |
Test helpers on top of Testing Library queries. |
@shadow-js/router |
dependency | createRouter, defineRoute, A, Outlet, useParams, ... |
Client routing. @shadow-js/router/core is the framework-independent part. |
@shadow-js/dusk |
dependency | dusk (@shadow-js/dusk/vite), createHandler, serverFn (@shadow-js/dusk/server), serve (@shadow-js/dusk/node) |
The full-stack layer: file routes, server functions, a secure request handler, a Node server. See Dusk. |
@shadow-js/convex, @shadow-js/clerk |
dependency | useQuery, useMutation, ClerkProvider, SignedIn, ... |
First-class integrations; convex is a peer dependency, and @clerk/clerk-js is an optional peer needed only by mode="bundled". |
@shadow-js/forms, @shadow-js/forms/server |
dependency | useForm, Form, Field, FieldArray, useWizard, applyServerErrors, parseFormData, ... |
Schema-validated forms with typed paths and field arrays. See Forms. |
@shadow-js/i18n |
dependency | createI18n, I18nProvider, useI18n, Trans, createI18nMiddleware, ... |
Typed ICU messages, lazy catalogs, locale routing, SSR with zero hydration mismatch. See Internationalization. |
@shadow-js/ui |
dependency and the shadow-ui command |
cn, cva, ThemeProvider, useTheme, ... |
The shadcn-style kit: the CLI copies components built on @shadow-js/primitives into your project. See the UI kit. |
create-shadow-app |
npm create |
n/a | The project scaffolder. |
@shadow-js/dusk is the full-stack layer: see Dusk.
Where to read next#
- Guides for each topic start at State with useStore.
- Every export, with signature and example, is under the API reference.
- All build-time options are on one page: Configuration.