Overview
Glyph uses React Context for all global state management. No Redux, no Zustand—just plain React.
Contexts are layered: SpaceContext wraps FileTreeContext wraps ViewContext, etc.
Context Hierarchy
function App () {
return (
< SpaceProvider > { /* Space lifecycle */ }
< FileTreeProvider > { /* Files, tags, active file */ }
< ViewProvider > { /* Active view document */ }
< UIProvider > { /* Sidebar, search state */ }
< EditorProvider > { /* TipTap editor instance */ }
< AppShell />
</ EditorProvider >
</ UIProvider >
</ ViewProvider >
</ FileTreeProvider >
</ SpaceProvider >
);
}
SpaceContext
Location : src/contexts/SpaceContext.tsx
Manages space lifecycle (create, open, close).
State Shape
interface SpaceContextValue {
// App metadata
info : AppInfo | null ; // App name, version
// Current space
spacePath : string | null ; // '/Users/me/my-space'
spaceSchemaVersion : number | null ; // 1
// History
lastSpacePath : string | null ; // For "Continue" button
recentSpaces : string []; // Up to 20 recent paths
// Index state
isIndexing : boolean ; // Index rebuild in progress
// Lifecycle
settingsLoaded : boolean ; // Settings loaded from disk
error : string ; // Error message
setError : ( error : string ) => void ;
// Actions
onOpenSpace : () => Promise < void >;
onOpenSpaceAtPath : ( path : string ) => Promise < void >;
onContinueLastSpace : () => Promise < void >;
onCreateSpace : () => Promise < void >;
closeSpace : () => Promise < void >;
startIndexRebuild : () => Promise < void >;
}
Usage
Opening a Space
Checking Space State
import { useSpace } from '@/contexts/SpaceContext' ;
function WelcomeScreen () {
const { onOpenSpace , onContinueLastSpace , lastSpacePath } = useSpace ();
return (
< div >
< Button onClick = { onOpenSpace } > Open Space </ Button >
{ lastSpacePath && (
< Button onClick = { onContinueLastSpace } >
Continue : { lastSpacePath }
</ Button >
)}
</ div >
);
}
FileTreeContext
Location : src/contexts/FileTreeContext.tsx
Manages file browser state and tag index.
State Shape
interface FileTreeContextValue {
// File tree data
rootEntries : FsEntry []; // Files/dirs at root
childrenByDir : Record < string , FsEntry []>; // Cached children by dir path
expandedDirs : Set < string >; // Which dirs are expanded
// Updaters (for hooks to modify state)
updateRootEntries : ( next : FsEntry [] | (( prev : FsEntry []) => FsEntry [])) => void ;
updateChildrenByDir : ( next : ...) => void ;
updateExpandedDirs : ( next : Set < string > | (( prev : Set < string >) => Set < string >)) => void ;
// Active file
activeFilePath : string | null ; // 'notes/example.md'
setActiveFilePath : ( path : string | null ) => void ;
// Derived state (computed from activeFilePath)
activeNoteId : string | null ; // Same as activeFilePath if .md
activeNoteTitle : string | null ; // Filename without extension
// Tag index
tags : TagCount []; // [{ tag: 'research', count: 42 }]
tagsError : string ;
refreshTags : () => Promise < void >;
}
Usage
Reading File Tree
Updating State
import { useFileTreeContext } from '@/contexts/FileTreeContext' ;
function FileTreePane () {
const { rootEntries , expandedDirs } = useFileTreeContext ();
return (
< div >
{ rootEntries . map ( entry => (
< FileTreeItem
key = {entry. rel_path }
entry = { entry }
isExpanded = {expandedDirs.has(entry.rel_path)}
/>
))}
</ div >
);
}
ViewContext
Location : src/contexts/ViewContext.tsx
Manages “view documents” (folder, tag, search, database views).
State Shape
interface ViewContextValue {
activeViewDoc : ViewDoc | null ; // Current view (folder, tag, etc.)
setActiveViewDoc : ( doc : ViewDoc | null ) => void ;
isLoadingView : boolean ;
viewError : string ;
}
type ViewDoc =
| FolderViewDoc
| TagViewDoc
| SearchViewDoc
| DatabaseViewDoc ;
interface FolderViewDoc {
type : 'folder' ;
dir : string ;
files : FsEntry [];
subfolders : FolderViewFolder [];
note_previews : ViewNotePreview [];
}
Usage
import { useViewContext } from '@/contexts/ViewContext' ;
function MainContent () {
const { activeViewDoc } = useViewContext ();
if ( activeViewDoc ?. type === 'folder' ) {
return < FolderView doc ={ activeViewDoc } />;
}
if ( activeViewDoc ?. type === 'database' ) {
return < DatabasePane doc ={ activeViewDoc } />;
}
return < MarkdownEditorPane />;
}
UIContext
Location : src/contexts/UIContext.tsx
Manages UI chrome state (sidebar, search, modals).
State Shape
interface UIContextValue {
// Sidebar
activePane : 'files' | 'tags' | 'ai' | 'tasks' ;
setActivePane : ( pane : 'files' | 'tags' | 'ai' | 'tasks' ) => void ;
isSidebarCollapsed : boolean ;
toggleSidebar : () => void ;
// Command palette (Cmd+K)
isCommandPaletteOpen : boolean ;
openCommandPalette : () => void ;
closeCommandPalette : () => void ;
// Search
searchQuery : string ;
setSearchQuery : ( query : string ) => void ;
// Preview pane
activePreviewPath : string | null ;
setActivePreviewPath : ( path : string | null ) => void ;
}
Usage
Sidebar Panes
Command Palette
function SidebarHeader () {
const { activePane , setActivePane } = useUIContext ();
return (
< div >
< IconButton
onClick = {() => setActivePane ( 'files' )}
data - active = { activePane === 'files' }
>
< FileIcon />
</ IconButton >
< IconButton
onClick = {() => setActivePane ( 'ai' )}
data - active = { activePane === 'ai' }
>
< SparkleIcon />
</ IconButton >
</ div >
);
}
EditorContext
Location : src/contexts/EditorContext.tsx
Manages TipTap editor instance.
State Shape
import type { Editor } from '@tiptap/react' ;
interface EditorContextValue {
editor : Editor | null ; // TipTap editor instance
setEditor : ( editor : Editor | null ) => void ;
isEditing : boolean ; // Focus state
saveState : 'saved' | 'saving' | 'unsaved' | 'error' ;
setSaveState : ( state : 'saved' | 'saving' | 'unsaved' | 'error' ) => void ;
}
Usage
Creating Editor
Using Editor
import { useEditor } from '@tiptap/react' ;
import { useEditorContext } from '@/contexts/EditorContext' ;
function MarkdownEditorPane () {
const { setEditor , setSaveState } = useEditorContext ();
const editor = useEditor ({
extensions: [ /* ... */ ],
onUpdate : () => {
setSaveState ( 'unsaved' );
debouncedSave ();
},
onFocus : () => setIsEditing ( true ),
onBlur : () => setIsEditing ( false ),
});
useEffect (() => {
setEditor ( editor );
return () => setEditor ( null );
}, [ editor , setEditor ]);
return < EditorContent editor ={ editor } />;
}
Custom Context Pattern
All contexts follow this pattern:
Define context value type
interface MyContextValue {
data : string ;
setData : ( data : string ) => void ;
}
Create context with null default
const MyContext = createContext < MyContextValue | null >( null );
Create provider component
export function MyProvider ({ children } : { children : ReactNode }) {
const [ data , setData ] = useState ( '' );
const value = useMemo < MyContextValue >(
() => ({ data , setData }),
[ data ]
);
return (
< MyContext . Provider value = { value } >
{ children }
</ MyContext . Provider >
);
}
Create typed hook
export function useMyContext () : MyContextValue {
const ctx = useContext ( MyContext );
if ( ! ctx ) {
throw new Error ( 'useMyContext must be used within MyProvider' );
}
return ctx ;
}
Split Contexts
Instead of one giant context:
interface AppContextValue {
user : User ;
theme : Theme ;
files : File [];
// ... 20 more fields
}
// Every component re-renders when ANY field changes!
Use multiple small contexts:
< UserProvider >
< ThemeProvider >
< FileProvider >
{ /* Components only re-render when their context changes */ }
</ FileProvider >
</ ThemeProvider >
</ UserProvider >
Memoize Context Value
Always memoize the context value:
const value = useMemo < MyContextValue >(
() => ({ data , setData }),
[ data ] // Only recompute when data changes
);
Without useMemo, context consumers re-render on every provider render.
Selector Pattern
For large contexts, expose selectors:
interface FileTreeContextValue {
// Instead of exposing entire state:
// state: { rootEntries, childrenByDir, ... }
// Expose specific selectors:
useRootEntries : () => FsEntry [];
useChildrenByDir : () => Record < string , FsEntry []>;
useExpandedDirs : () => Set < string >;
}
// Components only subscribe to what they use
function FileList () {
const rootEntries = useFileTreeContext (). useRootEntries ();
// Only re-renders when rootEntries changes
}
Next Steps
Components Component architecture