Documentation / Tutorial / Part 1

Part 1: Setting Up Your Project

This part of the Essentials Tutorial takes you from an empty folder to a running TypeScript project with @causl/core installed and a first graph wired up. By the end you will have committed a value, read it back, and watched a derived node recompute — the smallest example the engine has to support, and the foundation everything else in the tutorial builds on.

What you'll learn

1. Scaffold the project

Causl is a plain library — there is no Causl CLI and nothing about your build tooling needs to know it exists. We recommend Vite for new projects because it keeps the cold-start fast and its TypeScript defaults are sensible. If you have a strong preference for Create React App, Next.js, Remix, or a non-React tool like SvelteKit, Causl works there too; the differences are confined to which packages you install in Part 5.

Open a terminal and run:

pnpm create vite@latest incident-tracker --template react-ts cd incident-tracker pnpm install

If you prefer npm or yarn, the equivalents are npm create vite@latest … and yarn create vite …. Either way you should end up with a folder containing package.json, tsconfig.json, a src/ directory, and a working pnpm dev command.

Using Create React App? Run npx create-react-app incident-tracker --template typescript instead. Everything else in the tutorial is identical — Causl does not care which bundler compiled it.

2. Install Causl

The engine itself lives in @causl/core. That single package contains createCausl, the canonical seven-method Graph surface, and the structured error classes you will branch on. You do not need anything else to follow Parts 1 through 4:

pnpm add @causl/core

The remaining packages are introduced as the tutorial needs them. For reference, this is the full set you will install across the eight parts:

PackageIntroduced inPurpose
@causl/corePart 1The engine — createCausl(), inputs, deriveds, commits.
@causl/reactPart 5React bindings — useCauslNode, CauslProvider.
@causl/syncPart 6Async resource lifecycle helpers.
@causl/persistencePart 7Snapshot/restore + storage adapters.
@causl/checkerPart 8Static lint + bounded model checker.

You can install everything up front if you prefer one shopping trip; nothing is pulled into the bundle until you import from it.

3. Lay out the source tree

Causl does not impose a folder convention, but the tutorial uses a layout that scales well from "single graph in src/state/" up to feature-sliced applications. Replace Vite's src/ with the following structure:

src/ ├── main.tsx # React entry point (added in Part 5) ├── App.tsx # Root component (added in Part 5) └── state/ ├── graph.ts # createCausl() call — the single source of truth ├── inputs.ts # graph.input(...) declarations ├── derived.ts # graph.derived(...) declarations └── actions.ts # functions that wrap graph.commit(...)

The split matters for two reasons. First, it keeps the shape of your state (inputs and deriveds) physically separate from the transitions that mutate it (commit-wrapping action functions). That is the same separation Redux enforces with slices and dispatched actions, and it makes both halves easier to test in isolation. Second, exporting nodes from dedicated modules makes them tree-shakeable and gives the static checker in Part 8 a stable surface to point at.

4. Create the graph

Open src/state/graph.ts and replace its contents with this:

// src/state/graph.ts import { createCausl } from '@causl/core' export const graph = createCausl()

That's the whole entry point. createCausl() returns a fresh Graph with its own independent GraphTime — there is no module-level state inside the engine and no implicit singleton. Calling createCausl() twice gives you two separate universes that cannot see each other; that property is what makes Causl easy to use in tests and in SSR contexts where each request needs its own state.

Detailed explanation: why a module-scoped export? Exporting a singleton instance from graph.ts matches the createStore pattern Redux apps use. In tests, you instead call createCausl() inside beforeEach to get a fresh graph per test. In SSR (Next.js, Remix), you create one per request and pass it through React context — exactly the CauslProvider shape we wire up in Part 5.

5. Add your first input and derivation

Now register a single mutable cell and a computed view over it. Create src/state/inputs.ts:

// src/state/inputs.ts import { graph } from './graph' export const count = graph.input('count', 0)

And src/state/derived.ts:

// src/state/derived.ts import { graph } from './graph' import { count } from './inputs' export const doubled = graph.derived('doubled', g => g(count) * 2)

Two things are worth pausing on. First, both graph.input() and graph.derived() take a stable string id as their first argument. That id lives in your information-model namespace, separate from the variable name — so even after a refactor, the same logical cell still has the same id, and the bounded model checker can identify it across runs. Part 2 covers the id rules in detail.

Second, the second argument to graph.derived() is a closure that receives a tracked accessor (bound to g above — the single-letter form is idiomatic for inline arrows; any name works). The engine infers the derivation's dependency set from the accessor calls it observes — there is no manual "depends on" list to keep in sync. A derivation that switches inputs on an if branch naturally rewires its dependency set on the next evaluation.

6. Run your first commit

The only operation that advances GraphTime and writes to inputs is graph.commit(). Add src/state/actions.ts:

// src/state/actions.ts import { graph } from './graph' import { count } from './inputs' export function increment(): void { graph.commit('increment', tx => { tx.set(count, graph.read(count) + 1) }) }

And finally, a throwaway script to prove the pieces fit together. Edit Vite's generated src/main.tsx down to just this for now:

// src/main.tsx import { graph } from './state/graph' import { count } from './state/inputs' import { doubled } from './state/derived' import { increment } from './state/actions' console.log('initial count ', graph.read(count)) // 0 console.log('initial doubled ', graph.read(doubled)) // 0 increment() increment() increment() console.log('count ', graph.read(count)) // 3 console.log('doubled ', graph.read(doubled)) // 6

Run pnpm dev, open the browser console, and you should see exactly those six lines. If you do — congratulations, the engine is real on your machine. You have an input, a derivation that recomputed when its dependency changed, three commits that each advanced time by one, and reads that returned the post-commit values.

What's next

You have the smallest possible Causl app. It still has one input, one derived, and no UI; in Part 2 we model the incident-tracker domain as a richer graph and introduce the id and namespace rules that scale beyond a single counter.

← Tutorial index Part 2: Building Your First Graph →