---
title: renderToString
---

# renderToString

Renders a JSX tree into an HTML string.

## Signature

```ts
declare function renderToString(node: unknown): Promise<string>;
```

`node` is a single JSX node: a component, an element, a fragment (`<></>` for
several siblings), or any [`Renderable`](/api/core/jsx-types#renderable). It is
typed `unknown` because JSX's own typing already constrains what compiles.

## Usage

```tsx
import { renderToString } from "@vincle/core";

const Page = ({ title }: { title: string }) => (
  <html>
    <head>
      <title>{title}</title>
    </head>
    <body>
      <h1>{title}</h1>
    </body>
  </html>
);

const html = await renderToString(<Page title="My Site" />);
```

## Async components

Any component in the tree can be `async`: the component that needs data fetches
it, and the returned promise resolves once the whole tree has rendered.

```tsx
import { renderToString } from "@vincle/core";

declare const db: {
  posts: {
    findAll(opts: { limit: number }): Promise<{ id: number; slug: string; title: string }[]>;
  };
};

const Feed = async () => {
  const posts = await db.posts.findAll({ limit: 10 });
  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>
          <a href={"/posts/" + p.slug}>{p.title}</a>
        </li>
      ))}
    </ul>
  );
};

const html = await renderToString(<Feed />);
```

Components run one at a time, in document order, never in parallel, including
in arrays, iterables and precompiled templates. So two siblings touching `Scope`
never race, and when one throws or rejects, the ones after it are not invoked.
A promise you created before rendering is already running; for concurrent
regions, use [`Defer` and `Slot`](/integration/components) from `@vincle/flow`.

## Concurrent renders

Concurrent calls are safe, even when components use Providers or `Scope.with`:
each render keeps its own values, with no cross-request leakage.

```tsx
const [pageA, pageB] = await Promise.all([renderToString(<PageA />), renderToString(<PageB />)]);
```
