Skip to content

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.

The SDK hooks require two providers: TanStack Query’s QueryClientProvider (which you manage) and GithostedProvider (which connects hooks to a githosted client).

app.tsx
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>
);
}

useFileTree returns a standard TanStack Query result with data, isLoading, and error fields.

components/file-tree.tsx
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>
);
}

useFile returns { content, headSha, blobSha } in its data field.

components/file-viewer.tsx
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>
);
}
components/commit-log.tsx
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>
);
}

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.

components/file-editor.tsx
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>
);
}
components/repo-browser.tsx
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>
);
}
  • QueryClientProvider must wrap GithostedProvider. The SDK hooks use TanStack Query internally but do not create their own QueryClient — you control cache settings, devtools, and persistence.
  • GithostedProvider accepts either a client instance or token/baseUrl props. 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 useWriteFile or useDeleteFile call, the file tree, file content, commit log, and branch caches are all invalidated. Components re-fetch automatically.
  • expectedHead enables optimistic concurrency in the editor. If another user saved between your read and write, the mutation will fail with StaleHeadError instead of silently overwriting.