Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork167
⛩️ The minimal React framework
License
wakujs/waku
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
⛩️ The minimal React framework
visitwaku.gg ornpm create waku@latest
Waku(wah-ku) orわく means “framework” in Japanese. As the minimal React framework, it’s designed to accelerate the work of developers at startups and agencies building small to medium-sized React projects. These include marketing websites, light ecommerce, and web applications.
We recommend other frameworks for heavy ecommerce or enterprise applications. Waku is a lightweight alternative bringing a fun developer experience to the server components era. Yes, let’s make React development fun again!
Waku is in rapid development and some features are currently missing. Please try it on non-production projects and report any issues you may encounter. Expect that there will be some breaking changes on the road towards a stable v1 release. Contributors are welcome.
Start a new Waku project with thecreate command for your preferred package manager. It will scaffold a new project with our defaultWaku starter.
npm create waku@latest
Node.js version requirement:^24.0.0 or^22.12.0 or^20.19.0
While there’s a bit of a learning curve to modern React rendering, it introduces powerful new patterns of full-stack composability that are only possible with the advent ofserver components.
So please don’t be intimidated by the'use client' directive! Once you get the hang of it, you’ll appreciate how awesome it is to flexibly move server-client boundaries with a single line of code as your full-stack React codebase evolves over time. It’s way simpler than maintaining separate codebases for your backend and frontend.
And please don’t fret about client components! Even if you only lightly optimize towards server components, your client bundle size will be smaller than traditional React frameworks, which are always 100% client components.
Future versions of Waku may provide additional opt-in APIs to abstract some of the complexity away for an improved developer experience.
Server components can be made async and can securely perform server-side logic and data fetching. Feel free to access the local file-system and import heavy dependencies since they aren’t included in the client bundle. They have no state, interactivity, or access to browser APIs since they runexclusively on the server.
// server componentimportdbfrom'some-db';import{Gallery}from'../components/gallery';exportconstStore=async()=>{constproducts=awaitdb.query('SELECT * FROM products');return<Galleryproducts={products}/>;};
A'use client' directive placed at the top of a file will create a server-client boundary when imported into a server component. All components imported below the boundary will be hydrated to run in the browser as well. They can use all traditional React features such as state, effects, and event handlers.
// client component'use client';import{useState}from'react';exportconstCounter=()=>{const[count,setCount]=useState(0);return(<><div>Count:{count}</div><buttononClick={()=>setCount((c)=>c+1)}>Increment</button></>);};
Simple React components thatmeet all of the rules of both server and client components can be imported into either server or client components without affecting the server-client boundary.
// shared componentexportconstHeadline=({ children})=>{return<h3>{children}</h3>;};
Server components can import client components and doing so will create a server-client boundary. Client components cannot import server components, but they can accept server components as props such aschildren. For example, you may want to add global context providers this way.
// ./src/pages/_layout.tsximport{Providers}from'../components/providers';exportdefaultasyncfunctionRootLayout({ children}){return(<Providers><main>{children}</main></Providers>);}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
// ./src/components/providers.tsx'use client';import{Provider}from'jotai';exportconstProviders=({ children})=>{return<Provider>{children}</Provider>;};
Waku provides static prerendering (SSG) and server-side rendering (SSR) options for both layouts and pages including all of their serverand client components. Note that SSR is a distinct concept from RSC.
Each layout and page in Waku is composed of a React component hierarchy.
It begins with a server component at the top of the tree. Then at points down the hierarchy, you’ll eventually import a component that needs client component APIs. Mark this file with a'use client' directive at the top. When imported into a server component, it will create a server-client boundary. Below this point, all imported components are hydrated and will run in the browser as well.
Server components can be rendered below this boundary, but only via composition (e.g.,children props). Together they forma new “React server” layer that runsbefore the traditional “React client” layer with which you’re already familiar.
Client components are still server-side rendered as SSR is separate from RSC. Please see thelinked diagrams for a helpful visual.
To learn more about the modern React architecture, we recommendMaking Sense of React Server Components andThe Two Reacts.
Waku provides a minimal file-based “pages router” experience built for the server components era.
Its underlyinglow-level API is also available for those that prefer programmatic routing. This documentation covers file-based routing since many React developers prefer it, but please feel free to try both and see which you like more!
The directory for file-based routing in Waku projects is./src/pages.
Layouts and pages can be created by making a new file with two exports: a default function for the React component and a namedgetConfig function that returns a configuration object to specify the render method and other options.
Waku currently supports two rendering options:
'static'for static prerendering (SSG)'dynamic'for server-side rendering (SSR)
Layouts, pages, and slices are allstatic by default, while api handlers default todynamic.
For example, you can statically prerender a global header and footer in the root layout at build time, but dynamically render the rest of a home page at request time for personalized user experiences.
// ./src/pages/_layout.tsximport'../styles.css';import{Providers}from'../components/providers';import{Header}from'../components/header';import{Footer}from'../components/footer';// Create root layoutexportdefaultasyncfunctionRootLayout({ children}){return(<Providers><Header/><main>{children}</main><Footer/></Providers>);}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
// ./src/pages/index.tsx// Create home pageexportdefaultasyncfunctionHomePage(){constdata=awaitgetData();return(<><h1>{data.title}</h1><div>{data.content}</div></>);}constgetData=async()=>{/* ... */};exportconstgetConfig=async()=>{return{render:'dynamic',}asconst;};
Pages render a single route, segment route, or catch-all route based on the file system path (conventions below). All page components automatically receive two props related to the rendered route:path (string) andquery (string).
Pages can be rendered as a single route (e.g.,about.tsx orblog/index.tsx).
// ./src/pages/about.tsx// Create about pageexportdefaultasyncfunctionAboutPage(){return<>{/* ...*/}</>;}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
// ./src/pages/blog/index.tsx// Create blog index pageexportdefaultasyncfunctionBlogIndexPage(){return<>{/* ...*/}</>;}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
Segment routes (e.g.,[slug].tsx or[slug]/index.tsx) are marked with brackets.
The rendered React component automatically receives a prop named by the segment (e.g.,slug) with the value of the rendered segment (e.g.,'introducing-waku').
If statically prerendering a segment route at build time, astaticPaths array must also be provided.
// ./src/pages/blog/[slug].tsximporttype{PageProps}from'waku/router';// Create blog article pagesexportdefaultasyncfunctionBlogArticlePage({ slug,}:PageProps<'/blog/[slug]'>){constdata=awaitgetData(slug);return<>{/* ...*/}</>;}constgetData=async(slug)=>{/* ... */};exportconstgetConfig=async()=>{return{render:'static',staticPaths:['introducing-waku','introducing-pages-router'],}asconst;};
// ./src/pages/shop/[category].tsximporttype{PageProps}from'waku/router';// Create product category pagesexportdefaultasyncfunctionProductCategoryPage({ category,}:PageProps<'/shop/[category]'>){constdata=awaitgetData(category);return<>{/* ...*/}</>;}constgetData=async(category)=>{/* ... */};exportconstgetConfig=async()=>{return{render:'dynamic',}asconst;};
Static paths (or other config values) can also be generated programmatically.
// ./src/pages/blog/[slug].tsximporttype{PageProps}from'waku/router';// Create blog article pagesexportdefaultasyncfunctionBlogArticlePage({ slug,}:PageProps<'/blog/[slug]'>){constdata=awaitgetData(slug);return<>{/* ...*/}</>;}constgetData=async(slug)=>{/* ... */};exportconstgetConfig=async()=>{conststaticPaths=awaitgetStaticPaths();return{render:'static', staticPaths,}asconst;};constgetStaticPaths=async()=>{/* ... */};
Routes can contain multiple segments (e.g.,/shop/[category]/[product]) by creating folders with brackets as well.
// ./src/pages/shop/[category]/[product].tsximporttype{PageProps}from'waku/router';// Create product category pagesexportdefaultasyncfunctionProductDetailPage({ category, product,}:PageProps<'/shop/[category]/[product]'>){return<>{/* ...*/}</>;}exportconstgetConfig=async()=>{return{render:'dynamic',}asconst;};
For static prerendering of nested segment routes, thestaticPaths array is instead composed of ordered arrays.
// ./src/pages/shop/[category]/[product].tsximporttype{PageProps}from'waku/router';// Create product detail pagesexportdefaultasyncfunctionProductDetailPage({ category, product,}:PageProps<'/shop/[category]/[product]'>){return<>{/* ...*/}</>;}exportconstgetConfig=async()=>{return{render:'static',staticPaths:[['same-category','some-product'],['same-category','another-product'],],}asconst;};
Catch-all or “wildcard” segment routes (e.g.,/app/[...catchAll]) are marked with an ellipsis before the name and have indefinite segments.
Wildcard routes receive a prop with segment values as an ordered array. For example, the/app/profile/settings route would receive acatchAll prop with the value['profile', 'settings']. These values can then be used to determine what to render in the component.
// ./src/pages/app/[...catchAll].tsximporttype{PageProps}from'waku/router';// Create dashboard pageexportdefaultasyncfunctionDashboardPage({ catchAll,}:PageProps<'/app/[...catchAll]'>){return<>{/* ...*/}</>;}exportconstgetConfig=async()=>{return{render:'dynamic',}asconst;};
Group routes allow you to organize routes into logical groups without affecting the URL structure. They're created by wrapping directory names in parentheses (e.g.,(group)). This is particularly useful for sharing layouts across multiple routes while keeping the URL clean.
For example, you might want a home page at/ that doesn't use a shared layout, but all other routes should share a common layout. This can be achieved by grouping those routes:
├── (main)│ ├── _layout.tsx│ ├── about.tsx│ └── contact.tsx└── index.tsxIn this structure,/about and/contact will use the layout from(main)/_layout.tsx, but/ (fromindex.tsx) will not.
// ./src/pages/(main)/_layout.tsximport{Header}from'../../components/header';import{Footer}from'../../components/footer';// Create shared layout for main pagesexportdefaultasyncfunctionMainLayout({ children}){return(<><Header/><main>{children}</main><Footer/></>);}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
// ./src/pages/(main)/about.tsxexportdefaultasyncfunctionAboutPage(){return<h1>About Us</h1>;}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
Group routes can be nested to create complex layout compositions. For instance, you could have a static layout at the group level and a dynamic layout nested within:
(main)├── (dynamic)│ ├── _layout.tsx # dynamic layout│ ├── dashboard.tsx│ └── profile.tsx└── _layout.tsx # static layoutThis allows for fine-grained control over rendering modes - some work can be done at build time (static) while other work happens at runtime (dynamic).
// ./src/pages/(main)/_layout.tsx// Static layout - runs at build timeexportdefaultasyncfunctionMainLayout({ children}){return<divclassName="main-container">{children}</div>;}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
// ./src/pages/(main)/(dynamic)/_layout.tsx// Dynamic layout - runs at request timeexportdefaultasyncfunctionDynamicLayout({ children}){constuserData=awaitfetchUserData();// Dynamic data fetchingreturn(<divclassName="dynamic-container"><UserContext.Providervalue={userData}>{children}</UserContext.Provider></div>);}exportconstgetConfig=async()=>{return{render:'dynamic',}asconst;};
Group routes are especially powerful for organizing complex applications where different sections need different layouts, state management, or data requirements while maintaining clean URLs.
Layouts are created with a special_layout.tsx file name and wrap the entire route and its descendents. They must accept achildren prop of typeReactNode. While not required, you will typically want at least a root layout.
The root layout placed at./pages/_layout.tsx is especially useful. It can be used for setting global styles, global metadata, global providers, global data, and global components, such as a header and footer.
// ./src/pages/_layout.tsximport'../styles.css';import{Providers}from'../components/providers';import{Header}from'../components/header';import{Footer}from'../components/footer';// Create root layoutexportdefaultasyncfunctionRootLayout({ children}){return(<Providers><linkrel="icon"type="image/png"href="/images/favicon.png"/><metaproperty="og:image"content="/images/opengraph.png"/><Header/><main>{children}</main><Footer/></Providers>);}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
// ./src/components/providers.tsx'use client';import{createStore,Provider}from'jotai';conststore=createStore();exportconstProviders=({ children})=>{return<Providerstore={store}>{children}</Provider>;};
Layouts are also helpful in nested routes. For example, you can add a layout at./pages/blog/_layout.tsx to add a sidebar to both the blog index and all blog article pages.
// ./src/pages/blog/_layout.tsximport{Sidebar}from'../../components/sidebar';// Create blog layoutexportdefaultasyncfunctionBlogLayout({ children}){return(<divclassName="flex"><div>{children}</div><Sidebar/></div>);}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
The attributes of<html>,<head>, or<body> elements can be customized with the root element API. Create a special_root.tsx file in the./src/pages directory that accepts achildren prop of typeReactNode.
// ./src/pages/_root.tsx// Create root elementexportdefaultasyncfunctionRootElement({ children}){return(<htmllang="en"><head></head><bodydata-version="1.0">{children}</body></html>);}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
Slices are reusable components that are defined in thesrc/pages/_slices directory. They allow you to compose pages by assembling components like normal React components while specifying alternate rendering patterns.
Slices are created by placing files in thesrc/pages/_slices directory. The slice ID corresponds to the filename, and nested slices use the full path as the ID.
src/pages ├── _slices │ ├── one.tsx │ ├── two.tsx │ └── nested │ └── three.tsx └── some-page.tsxEach slice file exports a default React component and agetConfig function that specifies the render method.
// ./src/pages/_slices/one.tsx// Create slice componentexportdefaultfunctionSliceOne(){return<p>🍕</p>;}exportconstgetConfig=()=>{return{render:'static',// default is 'static'};};
// ./src/pages/_slices/nested/three.tsx// Create nested slice componentexportdefaultfunctionSliceThree(){return<p>🍰</p>;}exportconstgetConfig=()=>{return{render:'dynamic',};};
Slices are used in pages and layouts by importing theSlice component from Waku and specifying the slice ID. Theslices array in the page'sgetConfig must include all slice IDs used on that page.
// ./src/pages/some-page.tsximport{Slice}from'waku';// Create page with slicesexportdefaultfunctionSomePage(){return(<div><Sliceid="one"/><Sliceid="two"/><Sliceid="nested/three"/></div>);}exportconstgetConfig=()=>{return{render:'static',slices:['one','two','nested/three'],};};
Lazy slices allow components to be requested independently from the page they are used on, similar to Astro's server islands feature. This is useful for components that will be dynamically rendered on otherwise static pages.
Lazy slices are marked with thelazy prop and can include afallback component to display while loading.
// ./src/pages/some-page.tsximport{Slice}from'waku';// Create page with lazy sliceexportdefaultfunctionSomePage(){return(<div><Sliceid="one"/><Sliceid="two"lazyfallback={<p>Two is loading...</p>}/></div>);}exportconstgetConfig=()=>{return{render:'static',slices:['one'],// Note: 'two' is lazy, so it is not included};};
This allows you to have adynamic slice component while keeping the rest of the page static.
The<Link /> component should be used for internal links. It accepts ato prop for the destination, which is automatically prefetched ahead of the navigation.
// ./src/pages/index.tsximport{Link}from'waku';exportdefaultasyncfunctionHomePage(){return(<><h1>Home</h1><Linkto="/about">About</Link></>);}
TheuseRouter hook can be used to inspect the current route or perform programmatic navigation.
Therouter object has two properties related to the current route:path (string) andquery (string).
'use client';import{useRouter}from'waku';exportconstComponent=()=>{const{ path, query}=useRouter();return(<><div>current path:{path}</div><div>current query:{query}</div></>);};
Therouter object also contains several methods for programmatic navigation:
router.push(to: string)- navigate to the provided routerouter.prefetch(to: string)- prefetch the provided routerouter.replace(to: string)- replace the current history entryrouter.reload()- reload the current routerouter.back()- navigate to the previous entry in the session historyrouter.forward()- navigate to the next entry in the session history
'use client';import{useRouter}from'waku';exportconstComponent=()=>{constrouter=useRouter();return(<><buttononClick={()=>router.push('/')}>Home</button><buttononClick={()=>router.back()}>Back</button></>);};
Waku automatically hoists any title, meta, and link tags to the document head. That means adding meta tags is as simple as adding them to any of your layout or page components.
// ./src/pages/_layout.tsxexportdefaultasyncfunctionRootLayout({ children}){return(<><linkrel="icon"type="image/png"href="/images/favicon.png"/><metaproperty="og:image"content="/images/opengraph.png"/>{children}</>);}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
// ./src/pages/index.tsxexportdefaultasyncfunctionHomePage(){return(<><title>Waku</title><metaname="description"content="The minimal React framework"/><h1>Waku</h1><div>Hello world!</div></>);}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
Metadata can also be generated programmatically.
// ./src/pages/index.tsxexportdefaultasyncfunctionHomePage(){return(<><Head/><div>{/* ...*/}</div></>);}constHead=async()=>{constmetadata=awaitgetMetadata();return(<><title>{metadata.title}</title><metaname="description"content={metadata.description}/></>);};constgetMetadata=async()=>{/* ... */};exportconstgetConfig=async()=>{return{render:'static',}asconst;};
Install any required dev dependencies (e.g.,npm i -D tailwindcss @tailwindcss/vite) and set up any required configuration (e.g.,waku.config.ts). Then create your global stylesheet (e.g.,./src/styles.css) and import it into the root layout.
// ./src/pages/_layout.tsximport'../styles.css';exportdefaultasyncfunctionRootLayout({ children}){return<>{children}</>;}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
/* ./src/styles.css */@import'tailwindcss';
// ./waku.config.tsimport{defineConfig}from'waku/config';importtailwindcssfrom'@tailwindcss/vite';exportdefaultdefineConfig({vite:{plugins:[tailwindcss()],},});
Static assets such as images, fonts, stylesheets, and scripts can be placed in a special./public folder of the Waku project root directory. The public directory structure is served relative to the/ base path.
// assuming image is saved at `/public/images/logo.svg`exportconstLogo=()=>{return(<><imgsrc="/images/logo.svg"/></>);};
Files placed in a special./private folder of the Waku project root directory can be securely accessed in React server components.
exportdefaultasyncfunctionHomePage(){constfile=readFileSync('./private/README.md','utf8');return<>{/* ...*/}</>;}exportconstgetConfig=async()=>{return{render:'static',}asconst;};
All of the wonderful patterns enabled by React server components are supported. For example, you can compile MDX files or perform code syntax highlighting on the server with zero impact on the client bundle size.
// ./src/pages/blog/[slug].tsximporttype{PageProps}from'waku/router';import{MDX}from'../../components/mdx';import{getArticle,getStaticPaths}from'../../lib/blog';exportdefaultasyncfunctionBlogArticlePage({ slug,}:PageProps<'/blog/[slug]'>){constarticle=awaitgetArticle(slug);return(<><title>{article.frontmatter.title}</title><h1>{article.frontmatter.title}</h1><MDX>{article.content}</MDX></>);}exportconstgetConfig=async()=>{conststaticPaths=awaitgetStaticPaths();return{render:'static', staticPaths,}asconst;};
Data should be fetched on the server when possible for the best user experience, but all data fetching libraries such as React Query are compatible with Waku.
Data mutations can be performed viaserver actions or API endpoints.
Create API routes by making a new file in the special./src/pages/api directory and exporting one or more functions named after the HTTP methods that you want it to support:GET,HEAD,POST,PUT,DELETE,CONNECT,OPTIONS,TRACE, orPATCH. The name of the file determines the route it will be served from. Each function receives a standardRequest object and returns a standardResponse object.
// ./src/pages/api/contact.tsimportemailClientfrom'some-email';constclient=newemailClient(process.env.EMAIL_API_TOKEN!);exportconstPOST=async(request:Request):Promise<Response>=>{constbody=awaitrequest.json();if(!body.message){returnResponse.json({message:'Invalid'},{status:400});}try{awaitclient.sendEmail({From:'noreply@example.com',To:'someone@example.com',Subject:'Contact form submission',Body:body.message,});returnResponse.json({message:'Success'},{status:200});}catch(error){returnResponse.json({message:'Failure'},{status:500});}};
Alternatively, you may export a default function as a "catch-all" handler that responds to all request methods.
// ./src/pages/api/other-endpoint.tsexportdefaultfunctionhandler(request:Request):Response{returnResponse.json({message:'Default handler '+request.method},{status:200},);}
API routes are accessible at paths that match their file location. For example a file at./src/pages/api/contact.ts is available at/api/contact. You can call these endpoints from your client components using the standardFetch method.
'use client';import{useState}from'react';exportconstContactForm=()=>{const[message,setMessage]=useState('');const[status,setStatus]=useState('idle');consthandleSubmit=async(event)=>{event.preventDefault();setStatus('sending');try{constresponse=awaitfetch('/api/contact',{method:'POST',headers:{'Content-Type':'application/json',},body:JSON.stringify({ message}),});constdata=awaitresponse.json();if(response.status===200){setStatus('success');setMessage('');}else{setStatus('error');console.error('Error:',data.message);}}catch(error){setStatus('error');console.error('Error:',error);}};return(<formonSubmit={handleSubmit}><textareavalue={message}onChange={(event)=>setMessage(event.target.value)}placeholder="Your message..."required/><buttontype="submit"disabled={status==='sending'}>{status==='sending' ?'Sending...' :'Send Message'}</button>{status==='success'&&<p>Message sent!</p>}{status==='error'&&<p>Failed. Please try again.</p>}</form>);};
API routes are dynamic by default, but if you’re using them to create a static resource such as an XML document, you can export agetConfig function that returns a config object with the render property set to'static'.
// ./src/pages/api/rss.xml.tsexportconstGET=async()=>{constrssFeed=generateRSSFeed(items);returnnewResponse(rssFeed,{headers:{'Content-Type':'application/rss+xml',},});};exportconstgetConfig=async()=>{return{render:'static',}asconst;};constitems=[{title:`Announcing API routes`,description:`Easily add public API endpoints to your Waku projects.`pubDate:`Tue, 1 Apr 2025 00:00:00 GMT`,link:`https://waku.gg/blog/api-routes`,},// ...];constgenerateRSSFeed=(items)=>{constitemsXML=items.map((item)=>` <item> <title>${item.title}</title> <link>${item.link}</link> <pubDate>${item.pubDate}</pubDate> <description>${item.description}</description> </item> `,).join('');return` <?xml version="1.0" encoding="UTF-8" ?> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <channel> <atom:link href="https://waku.gg/api/rss.xml" rel="self" type="application/rss+xml" /> <title>Waku</title> <link>https://waku.gg</link> <description>The minimal React framework</description>${itemsXML} </channel> </rss> `;};
Server actions allow you to define and securely execute server-side logic directly from your React components without the need for manually setting up API endpoints, sendingPOST requests to them withfetch, or managing pending states and errors.
The'use server' directive marks an async function as a server action. Waku automatically creates a reference to the action that can be passed as props or imported into client components, which can then call the referenced function.
When the directive is placed at the top of a function body, it will mark that specific function as an action. Alternatively, the directive can be placed at the top of a file, which will markall exported functions as actions at once.
Be careful not to add the directive where inappropriate and inadvertently create unwanted endpoints. Endpoints created by server actions arenot secured unless you add your own authentication and authorization logic inside the function body.
The
'use server'directive has no relation to the'use client'directive. It doesnot mark a component as a server component and shouldnot be placed at the top of server components!
When creating an inline server action within a server component, it can be passed as props to a client component.
// ./src/pages/contact.tsximportdbfrom'some-db';exportdefaultasyncfunctionContactPage(){constsendMessage=async(message:string)=>{'use server';awaitdb.messages.create(message);};return<ContactFormsendMessage={sendMessage}/>;}
// ./src/components/contact-form.tsx'use client';import{useState}from'react';exportconstContactForm=({ sendMessage})=>{const[message,setMessage]=useState('');return(<><textareavalue={message}onChange={(event)=>setMessage(event.target.value)}rows={4}/><buttononClick={()=>sendMessage(message)}>Send message</button></>);};
When creating server actions in a separate file, they can be imported directly into client components.
When using a top-level
'use server'directive, note thatall exported functions will be made into API endpoints. So be careful only to export functions intended for this purpose. Add server-side logic to validate proper authentication and authorization if appropriate.
// ./src/actions/send-message.ts'use server';importdbfrom'some-db';exportasyncfunctionsendMessage(message:string){awaitdb.messages.create(message);}
// ./src/components/contact-button.tsx'use client';import{sendMessage}from'../actions/send-message';exportconstContactButton=()=>{constmessage=`Hello world!`;return<buttononClick={()=>sendMessage(message)}>Send message</button>;};
Actions can be invoked via event handlers such asonClick oronSubmit, as in the examples above, or in auseEffect hook, based on whichever conditions you choose.
They can also be invoked via anaction prop on native<form> elements. In this case the server action will automatically receive a parameter ofFormData with all of the form field values, including hidden ones.
// ./src/actions/send-message.ts'use server';importdbfrom'some-db';exportasyncfunctionsendMessage(formData:FormData){constmessage=formData.get('message');awaitdb.messages.create(message);}
// ./src/components/create-todo-button.tsx'use client';import{sendMessage}from'../actions/send-message';exportconstContactForm=()=>{return(<formaction={sendMessage}><textareaname="message"rows={4}/><inputtype="hidden"name="secret-message"value="This too!"/><buttontype="submit">Send message</button></form>);};
If you must pass additional arguments to a form action beyond its native form fields, you can use thebind method to create an extended server action with the extra arguments.
// ./src/components/create-todo-button.tsx'use client';import{sendMessage}from'../actions/send-message';exportconstContactForm=({ author='guest'})=>{constsendMessageWithAuthor=sendMessage.bind(null,author);return(<formaction={sendMessageWithAuthor}><textareaname="message"rows={4}/><buttontype="submit">Send message</button></form>);};
Server actions integrate with many other React APIs such as theuseTransition hook for handling pending states, theuseActionState hook for accessing returned values, and theuseOptimistic hook for performing optimistic UI updates.
See the talkWhat’s new in React 19? to learn more.
We recommendJotai for global React state management based on the atomic model’s performance and scalability, but Waku is compatible with all React state management libraries such as Zustand and Valtio.
We’re exploring a deeper integration of atomic state management into Waku to achieve the performance and developer experience of signals while preserving React’s declarative programming model.
It’s important to distinguish environment variables that must be kept secret from those that can be made public.
By default all environment variables are considered private and are accessible only in server components, which can be rendered exclusively in a secure environment. You must still take care not to inadvertently pass the variable as props to any client components.
A specialWAKU_PUBLIC_ prefix is required to make an environment variable public and accessible in client components. They will be present as cleartext in the production JavaScript bundle sent to users’ browsers.
Environment variables are available on the server via the WakugetEnv function and on the client viaimport.meta.env.
// server components can access both private and public variablesimport{getEnv}from'waku';exportconstServerComponent=async()=>{constsecretKey=getEnv('SECRET_KEY');return<>{/* ...*/}</>;};
// client components can only access public variables'use client';exportconstClientComponent=()=>{constpublicStatement=import.meta.env.WAKU_PUBLIC_HELLO;return<>{/* ...*/}</>;};
In Node.js environments,process.env may be used for compatibility.
// server components can access both private and public variablesexportconstServerComponent=async()=>{constsecretKey=process.env.SECRET_KEY;return<>{/* ...*/}</>;};
// client components can only access public variables'use client';exportconstClientComponent=()=>{constpublicStatement=process.env.WAKU_PUBLIC_HELLO;return<>{/* ...*/}</>;};
Waku projects can be deployed to Vercel with theVercel CLI automatically.
vercel
For adavanced users who want to avoid deploying functions, use the server entry file with vercel adapter and specifystatic option.
./src/server-entry.ts:
/// <reference types="vite/client" />import{fsRouter}from'waku';importadapterfrom'waku/adapters/vercel';exportdefaultadapter(fsRouter(import.meta.glob('./**/*.{tsx,ts}',{base:'./pages'})),{static:true},);
Waku projects can be deployed to Netlify with theNetlify CLI.
NETLIFY=1 npm run buildnetlify deploy
For adavanced users who want to avoid deploying functions, use the server entry file with netlify adapter and specifystatic option.
./src/server-entry.ts:
/// <reference types="vite/client" />import{fsRouter}from'waku';importadapterfrom'waku/adapters/netlify';exportdefaultadapter(fsRouter(import.meta.glob('./**/*.{tsx,ts}',{base:'./pages'})),{static:true},);
./src/server-entry.ts:
/// <reference types="vite/client" />import{fsRouter}from'waku';importadapterfrom'waku/adapters/cloudflare';exportdefaultadapter(fsRouter(import.meta.glob('./**/*.{tsx,ts}',{base:'./pages'})),);
npm run buildnpx wrangler dev# or deploy./src/server-entry.ts:
/// <reference types="vite/client" />import{fsRouter}from'waku';importadapterfrom'waku/adapters/deno';exportdefaultadapter(fsRouter(import.meta.glob('./**/*.{tsx,ts}',{base:'./pages'})),);
npm run builddeployctl deploy --prod dist/serve-deno.js --exclude node_modules
./src/server-entry.ts:
/// <reference types="vite/client" />import{fsRouter}from'waku';importadapterfrom'waku/adapters/aws-lambda';exportdefaultadapter(fsRouter(import.meta.glob('./**/*.{tsx,ts}',{base:'./pages'})),streaming:false,// optional, default is false);
npm run build
The handler entrypoint isdist/serve-asw-lambda.js: seeHono AWS Lambda Deploy Docs.
Please join our friendlyGitHub discussions orDiscord server to participate in the Waku community. Hope to see you there!
Waku is in active development and we’re seeking additional contributors. Check out ourroadmap for more information.
If you would like to contribute, please seeCONTRIBUTING.md!
About
⛩️ The minimal React framework
Resources
License
Contributing
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.