React Repo Browser
This example builds a minimal repo file browser using the React hooks from
@githosted/sdk/react. It demonstrates the provider setup, reading file trees
and file content, viewing commit history, and writing files with automatic cache
invalidation.
Provider setup
Section titled “Provider setup”The SDK hooks require two providers: TanStack Query’s QueryClientProvider (which
you manage) and GithostedProvider (which connects hooks to a githosted client).
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";import { GithostedProvider } from "@githosted/sdk/react";
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 10_000, // 10s before refetch }, },});
export default function App() { return ( <QueryClientProvider client={queryClient}> <GithostedProvider token="gw_your_workspace_token" onError={(err) => console.error("Githosted error:", err)} > <RepoBrowser repo="my-repo" /> </GithostedProvider> </QueryClientProvider> );}File tree component
Section titled “File tree component”useFileTree returns a standard TanStack Query result with data, isLoading,
and error fields.
import { useState } from "react";import { useFileTree, type FileEntry, type RepoRef } from "@githosted/sdk/react";
interface FileTreeProps { repo: RepoRef; path?: string; onSelectFile: (path: string) => void;}
export function FileTree({ repo, path = "", onSelectFile }: FileTreeProps) { const { data: entries, isLoading, error } = useFileTree(repo, path); const [expandedDirs, setExpandedDirs] = useState<Set<string>>(new Set());
if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>;
function toggleDir(dirPath: string) { setExpandedDirs((prev) => { const next = new Set(prev); if (next.has(dirPath)) next.delete(dirPath); else next.add(dirPath); return next; }); }
return ( <ul style={{ listStyle: "none", paddingLeft: 16 }}> {entries?.map((entry) => { const fullPath = path ? `${path}/${entry.name}` : entry.name;
if (entry.type === "directory") { const isExpanded = expandedDirs.has(fullPath); return ( <li key={entry.name}> <button onClick={() => toggleDir(fullPath)}> {isExpanded ? "v" : ">"} {entry.name}/ </button> {isExpanded && ( <FileTree repo={repo} path={fullPath} onSelectFile={onSelectFile} /> )} </li> ); }
return ( <li key={entry.name}> <button onClick={() => onSelectFile(fullPath)}> {entry.name} </button> </li> ); })} </ul> );}File viewer component
Section titled “File viewer component”useFile returns { content, headSha, blobSha } in its data field.
import { useFile, type RepoRef } from "@githosted/sdk/react";
interface FileViewerProps { repo: RepoRef; path: string;}
export function FileViewer({ repo, path }: FileViewerProps) { const { data: file, isLoading, error } = useFile(repo, path);
if (isLoading) return <div>Loading {path}...</div>; if (error) return <div>Error reading {path}: {error.message}</div>; if (!file) return null;
return ( <div> <h3>{path}</h3> <pre style={{ background: "#f5f5f5", padding: 16, overflow: "auto" }}> {file.content} </pre> <p style={{ fontSize: 12, color: "#666" }}> Head: {file.headSha.slice(0, 7)} | Blob: {file.blobSha.slice(0, 7)} </p> </div> );}Commit log component
Section titled “Commit log component”import { useCommitLog, type RepoRef } from "@githosted/sdk/react";
interface CommitLogProps { repo: RepoRef; path?: string;}
export function CommitLog({ repo, path }: CommitLogProps) { const { data: commits, isLoading } = useCommitLog(repo, { path, limit: 20, });
if (isLoading) return <div>Loading history...</div>;
return ( <table> <thead> <tr> <th>Hash</th> <th>Message</th> <th>Author</th> <th>Date</th> </tr> </thead> <tbody> {commits?.map((c) => ( <tr key={c.hash}> <td><code>{c.hash.slice(0, 7)}</code></td> <td>{c.subject}</td> <td>{c.authorName}</td> <td>{c.committedAt.toLocaleDateString()}</td> </tr> ))} </tbody> </table> );}File editor with useWriteFile
Section titled “File editor with useWriteFile”useWriteFile returns a TanStack useMutation result. On success, it
automatically invalidates the file tree, file content, and commit log caches
so all related components re-render with fresh data.
import { useState } from "react";import { useFile, useWriteFile, type RepoRef } from "@githosted/sdk/react";
interface FileEditorProps { repo: RepoRef; path: string;}
export function FileEditor({ repo, path }: FileEditorProps) { const { data: file } = useFile(repo, path); const writeFile = useWriteFile(repo); const [content, setContent] = useState<string | null>(null);
// Initialize editor content from fetched file const displayContent = content ?? file?.content ?? "";
async function handleSave() { writeFile.mutate({ path, content: displayContent, message: `Update ${path}`, expectedHead: file?.headSha, // optimistic concurrency }); }
return ( <div> <textarea value={displayContent} onChange={(e) => setContent(e.target.value)} rows={20} cols={80} /> <div> <button onClick={handleSave} disabled={writeFile.isPending} > {writeFile.isPending ? "Saving..." : "Save"} </button> {writeFile.isError && ( <span style={{ color: "red" }}> Save failed: {writeFile.error.message} </span> )} </div> </div> );}Putting it together
Section titled “Putting it together”import { useState } from "react";import { type RepoRef } from "@githosted/sdk/react";import { FileTree } from "./file-tree";import { FileViewer } from "./file-viewer";import { FileEditor } from "./file-editor";import { CommitLog } from "./commit-log";
interface RepoBrowserProps { repo: RepoRef;}
export function RepoBrowser({ repo }: RepoBrowserProps) { const [selectedFile, setSelectedFile] = useState<string | null>(null); const [editing, setEditing] = useState(false);
return ( <div style={{ display: "flex", gap: 24 }}> <div style={{ width: 250 }}> <h2>Files</h2> <FileTree repo={repo} onSelectFile={setSelectedFile} /> </div>
<div style={{ flex: 1 }}> {selectedFile && !editing && ( <> <FileViewer repo={repo} path={selectedFile} /> <button onClick={() => setEditing(true)}>Edit</button> </> )} {selectedFile && editing && ( <> <FileEditor repo={repo} path={selectedFile} /> <button onClick={() => setEditing(false)}>Cancel</button> </> )}
<h2>History{selectedFile ? ` for ${selectedFile}` : ""}</h2> <CommitLog repo={repo} path={selectedFile ?? undefined} /> </div> </div> );}Key points
Section titled “Key points”QueryClientProvidermust wrapGithostedProvider. The SDK hooks use TanStack Query internally but do not create their ownQueryClient— you control cache settings, devtools, and persistence.GithostedProvideraccepts either aclientinstance ortoken/baseUrlprops. When the token changes (e.g., user switches workspace), all cached queries from the previous scope are automatically invalidated.- Mutation hooks auto-invalidate. After a successful
useWriteFileoruseDeleteFilecall, the file tree, file content, commit log, and branch caches are all invalidated. Components re-fetch automatically. expectedHeadenables optimistic concurrency in the editor. If another user saved between your read and write, the mutation will fail withStaleHeadErrorinstead of silently overwriting.