note
Building a Virtual Filesystem in React
A small tree structure, a reducer, and a handful of commands — everything you need for a navigable in-memory filesystem.
·updated Aug 19, 2026·#react#typescript#terminal
const{Fragment:e,jsx:t,jsxs:n}=arguments[0];function _createMdxContent(r){const a={code:"code",p:"p",pre:"pre",...r.components};return n(e,{children:[n(a.p,{children:["A surprising amount of a terminal UI is just a filesystem simulator. ",t(a.code,{children:"ls"})," lists a directory, ",t(a.code,{children:"cd"})," changes it, ",t(a.code,{children:"cat"})," reads a file — none of that needs a real disk. It needs a tree, a cursor, and a few pure functions over both."]}),"\n",t(a.p,{children:"I modeled the VFS as a nested object where each node is either a directory with children or a file with content. TypeScript makes this pleasant with a discriminated union:"}),"\n",t(a.pre,{children:t(a.code,{className:"language-ts",children:'type Node =\n | { type: "dir"; name: string; children: Record }\n | { type: "file"; name: string; content: string };\n'})}),"\n",n(a.p,{children:["The current path lives in a ",t(a.code,{children:"useReducer"}),", and every command is a pure function of ",t(a.code,{children:"(state, args) => state"}),". That purity is the important part — it means the command history is replayable, tests are trivial, and there's no hidden state to drift out of sync."]}),"\n",n(a.p,{children:["Path resolution is the one place people reach for a library, but ~20 lines of code handles it: split the argument on ",t(a.code,{children:"/"}),", handle ",t(a.code,{children:"."})," and ",t(a.code,{children:".."}),", walk the tree, throw on a missing segment. The whole thing is shorter than the npm install would be."]}),"\n",n(a.p,{children:["The nicest payoff is that the VFS can be derived from real content. I walk the Velite collections at build time and produce a tree where ",t(a.code,{children:"projects/personal-site"})," maps to a real MDX document. So the filesystem isn't a toy — it's a navigation surface over the actual content graph."]}),"\n",t(a.p,{children:"If you're building any kind of command-driven UI, start with the data model. Get the tree right and the commands almost write themselves."})]})}return{default:function(e={}){const{wrapper:n}=e.components||{};return n?t(n,{...e,children:t(_createMdxContent,{...e})}):_createMdxContent(e)}};