{post.title}
{post.excerpt}
A React metaframework with TanStack Query-like data fetching, SSR, and Vite integration
npm install nitro-tsxA modern React framework that combines the best of SSR and SPA with powerful data fetching capabilities.
+route.tsx and +layout.tsx conventions$id, $slug)(group) syntax``bash`
npm install nitro-js@npm:nitro-tsx@latest
`typescript
// vite.config.ts
import { defineConfig } from 'vite';
import { nitro } from 'nitro-js/vite';
export default defineConfig({
plugins: [
...nitro({
ssr: false, // SPA mode
clientEntry: 'src/main.tsx'
})
],
});
`
`typescript
// src/main.tsx
import { createRoot } from 'react-dom/client';
import { NitroBrowser } from 'nitro-js/router';
import { QueryClientProvider, createQueryClient } from 'nitro-js/query';
const queryClient = createQueryClient();
createRoot(document.getElementById('root')!).render(
);
`
`typescript
// vite.config.ts
import { defineConfig } from 'vite';
import { nitro } from 'nitro-js/vite';
export default defineConfig({
plugins: [
...nitro({
ssr: true, // SSR mode
handlerPath: 'src/entry.server.tsx'
})
],
});
`
`typescript
// src/entry.server.tsx
import { renderToReadableStream } from 'react-dom/server';
import { NitroServer, createNitroHandler } from 'nitro-js/router';
import { QueryClientProvider, createQueryClient } from 'nitro-js/query';
export default async function handler(request) {
const queryClient = createQueryClient();
const context = await createNitroHandler()(request);
const stream = await renderToReadableStream(
);
return new Response(stream, {
headers: { 'Content-Type': 'text/html' }
});
}
`
`typescript
// src/entry.client.tsx
import { hydrateRoot } from 'react-dom/client';
import { NitroBrowser } from 'nitro-js/router';
import { QueryClientProvider, createQueryClient } from 'nitro-js/query';
const queryClient = createQueryClient();
hydrateRoot(
document.getElementById('root')!,
);
`
Create routes using file conventions:
``
src/app/
āāā +layout.tsx # Root layout
āāā +route.tsx # Home page (/)
āāā about/
ā āāā +route.tsx # About page (/about)
āāā posts/
ā āāā +route.tsx # Posts list (/posts)
ā āāā $id/
ā āāā +route.tsx # Post detail (/posts/:id)
āāā (admin)/
āāā dashboard/
āāā +route.tsx # Admin dashboard (/dashboard)
`typescript
// src/app/+layout.tsx
import { Outlet, Link } from 'nitro-js/router';
export default function RootLayout() {
return (
$3
`typescript
// src/app/posts/+route.tsx
import { useQuery } from 'nitro-js/query';export default function PostsPage() {
const { data: posts, isLoading } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(res => res.json()),
staleTime: 5 60 1000, // 5 minutes
});
if (isLoading) return
Loading posts...; return (
Blog Posts
{posts?.map(post => (
{post.title}
{post.excerpt}
))}
);
}
`Data Fetching
$3
`typescript
import { useQuery, createQueryKeys } from 'nitro-js/query';const postKeys = createQueryKeys('posts');
function PostsList() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: postKeys.lists(),
queryFn: fetchPosts,
staleTime: 5 60 1000,
refetchInterval: 30 * 1000,
});
return (
{isLoading && Loading...}
{error && Error: {error.message}}
{data?.map(post => )}
);
}
`$3
`typescript
import { useMutation, useInvalidateQueries } from 'nitro-js/query';function LikeButton({ postId }) {
const invalidateQueries = useInvalidateQueries();
const likeMutation = useMutation({
mutationFn: ({ postId }) => likePost(postId),
onMutate: async ({ postId }) => {
// Optimistic update
const previousPost = queryClient.getQueryData(['posts', postId]);
queryClient.setQueryData(['posts', postId], old => ({
...old,
likes: old.likes + 1
}));
return { previousPost };
},
onSuccess: (data, { postId }) => {
invalidateQueries(['posts', postId]);
},
onError: (error, variables, context) => {
// Rollback optimistic update
queryClient.setQueryData(['posts', variables.postId], context.previousPost);
},
});
return (
onClick={() => likeMutation.mutate({ postId })}
disabled={likeMutation.isPending}
>
{likeMutation.isPending ? 'Liking...' : 'Like'}
);
}
`$3
`typescript
import { useQuerySignal } from 'nitro-js/query';function ReactiveStats() {
const [statsSignal, { isLoading, refetch }] = useQuerySignal(
['stats'],
fetchStats,
{ refetchInterval: 10 * 1000 }
);
// Signal automatically updates the component
const stats = statsSignal();
return (
Live Stats
Users: {stats?.users}
Posts: {stats?.posts}
);
}
`State Management
`typescript
import { useSignal } from 'nitro-js/state';function Counter() {
const [count, setCount] = useSignal(0);
return (
Count: {count()}
);
}
`Examples
$3
Complete single-page application demonstrating:
- Client-side routing and navigation
- Data fetching with caching and background updates
- Optimistic mutations and error handling
- Signal-based reactive state
- Interactive components and real-time updates`bash
cd examples/spa-example
npm install
npm run dev
`$3
Full server-side rendering application showcasing:
- Streaming SSR with React 19
- Server-side data fetching and hydration
- SEO optimization and social sharing
- Real-time dashboard with analytics
- Progressive enhancement patterns`bash
cd examples/ssr-example
npm install
npm run dev
`API Reference
$3
`typescript
/* Nitro.js plugin options /
export interface NitroOptions {
/* Custom path to server entry (default: /src/entry.server.tsx) /
handlerPath?: string;
/* Custom path to client entry for SPA mode (default: /src/entry.client.tsx) /
clientEntry?: string;
/* React plugin config. /
reactPlugin?: Options;
/* Allows usage of the React compiler. /
reactCompiler?: boolean;
/* Whether to enable SSR (true) or use SPA mode (false). Default: true /
ssr?: boolean;
}
`$3
-
useQuery(options) - Fetch and cache data
- useMutation(options) - Perform mutations with optimistic updates
- useQuerySignal(key, fn, options) - Signal-based reactive queries
- useInvalidateQueries() - Invalidate cached queries
- usePrefetchQuery() - Prefetch data for better UX
- useSetQueryData() - Manually update cache$3
-
- Client-side router for SPA mode
- - Server-side router for SSR mode
- - Navigation component with view transitions
- - Render child routes in layouts$3
-
useSignal(initialValue)` - Reactive state primitive- Modern browsers: Chrome 90+, Firefox 88+, Safari 14+
- SSR: Node.js 18+ or Edge Runtime
- Progressive enhancement: Works without JavaScript
We welcome contributions! Please see our Contributing Guide for details.
MIT License - see LICENSE file for details.