In this article, I discuss how Blocks page is built on ui.shadcn.com. Blocks page has a lot of utilities used, hence I broke down this Blocks page analysis into 5 parts.
- shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 1
- shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 2
- shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 3
- shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 4
- shadcn-ui/ui codebase analysis: How is “Blocks” page built — Part 5
This is final part 5 where I will discuss the following:
- getBlock function
- BlockPreview component
- BlockDisplay
getBlock function
getBlock function uses that same function such as readFile, createTempSourceFile and project.createSourceFile. I explained about this in great detail in part 4.
To summarise, project.createSourceFile is an API provided by ts-morph to perform Typescript manipulations such as removing a variable from a file by access Typescript’s AST. This can simplify refactoring as it can save a lot of time in performing repetitive tasks such as renaming a property or function when dealing with Typescript code across a large code base.
// source: https://github.com/shadcn-ui/ui/blob/main/apps/www/lib/blocks.ts#L27
export async function getBlock(
name: string,
style: Style\["name"\] = DEFAULT\_BLOCKS\_STYLE
) {
const entry = Index\[style\]\[name\]
const content = await \_getBlockContent(name, style)
const chunks = await Promise.all(
entry.chunks?.map(async (chunk: BlockChunk) => {
const code = await readFile(chunk.file)
const tempFile = await createTempSourceFile(\`${chunk.name}.tsx\`)
const sourceFile = project.createSourceFile(tempFile, code, {
scriptKind: ScriptKind.TSX,
})
sourceFile
.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
.filter((node) => {
return node.getAttribute("x-chunk") !== undefined
})
?.map((component) => {
component
.getAttribute("x-chunk")
?.asKind(SyntaxKind.JsxAttribute)
?.remove()
})
return {
...chunk,
code: sourceFile
.getText()
.replaceAll(\`@/registry/${style}/\`, "@/components/"),
}
})
)
return blockSchema.parse({
style,
highlightedCode: content.code ? await highlightCode(content.code) : "",
...entry,
...content,
chunks,
type: "components:block",
})
}
This function attempts to remove nodes with “x-chunk” attribute. To my surprise, there’s some block examples that do contain this attribute as shown in the below image
and getBlock function returns the below object to a variable in BlockDisplay component.
return blockSchema.parse({
style,
highlightedCode: content.code ? await highlightCode(content.code) : "",
...entry,
...content,
chunks,
type: "components:block",
})
BlockDisplay Component
Now that we fully understand what happens behind the scenes when you call getBlock as shown in the below code, BlockDisplay uses a Promise.all and waits till it gets all the blocks.
export async function BlockDisplay({ name }: { name: string }) {
const blocks = await Promise.all(
styles.map(async (style) => {
const block = await getBlock(name, style.name)
const hasLiftMode = block?.chunks ? block?.chunks?.length > 0 : false
// Cannot (and don't need to) pass to the client.
delete block?.component
delete block?.chunks
return {
...block,
hasLiftMode,
}
})
)
if (!blocks?.length) {
return null
}
return blocks.map((block) => (
<BlockPreview key={\`${block.style}-${block.name}\`} block={block} />
))
}
and then BlockPreview is used to show a block example.
BlockPreview component
The below code is picked from BlockPreview component
"use client"
import \* as React from "react"
import { ImperativePanelHandle } from "react-resizable-panels"
import { cn } from "@/lib/utils"
import { useConfig } from "@/hooks/use-config"
import { useLiftMode } from "@/hooks/use-lift-mode"
import { BlockToolbar } from "@/components/block-toolbar"
import { Icons } from "@/components/icons"
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/registry/new-york/ui/resizable"
import { Tabs, TabsContent } from "@/registry/new-york/ui/tabs"
import { Block } from "@/registry/schema"
export function BlockPreview({
block,
}: {
block: Block & { hasLiftMode: boolean }
}) {
const \[config\] = useConfig()
const { isLiftMode } = useLiftMode(block.name)
const \[isLoading, setIsLoading\] = React.useState(true)
const ref = React.useRef<ImperativePanelHandle>(null)
if (config.style !== block.style) {
return null
}
return (
<Tabs
id={block.name}
defaultValue="preview"
className="relative grid w-full scroll-m-20 gap-4"
style={
{
"--container-height": block.container?.height,
} as React.CSSProperties
}
>
<BlockToolbar block={block} resizablePanelRef={ref} />
<TabsContent
value="preview"
className="relative after:absolute after:inset-0 after:right-3 after:z-0 after:rounded-lg after:bg-muted"
>
<ResizablePanelGroup direction="horizontal" className="relative z-10">
<ResizablePanel
ref={ref}
className={cn(
"relative rounded-lg border bg-background",
isLiftMode ? "border-border/50" : "border-border"
)}
defaultSize={100}
minSize={30}
>
{isLoading ? (
<div className="absolute inset-0 z-10 flex h-\[--container-height\] w-full items-center justify-center gap-2 text-sm text-muted-foreground">
<Icons.spinner className="h-4 w-4 animate-spin" />
Loading...
</div>
) : null}
<iframe
src={\`/blocks/${block.style}/${block.name}\`}
height={block.container?.height}
className="chunk-mode relative z-20 w-full bg-background"
onLoad={() => {
setIsLoading(false)
}}
/>
</ResizablePanel>
<ResizableHandle
className={cn(
"relative hidden w-3 bg-transparent p-0 after:absolute after:right-0 after:top-1/2 after:h-8 after:w-\[6px\] after:-translate-y-1/2 after:translate-x-\[-1px\] after:rounded-full after:bg-border after:transition-all after:hover:h-10 sm:block",
isLiftMode && "invisible"
)}
/>
<ResizablePanel defaultSize={0} minSize={0} />
</ResizablePanelGroup>
</TabsContent>
<TabsContent value="code">
<div
data-rehype-pretty-code-fragment
dangerouslySetInnerHTML={{ \_\_html: block.highlightedCode }}
className="w-full overflow-hidden rounded-md \[&\_pre\]:my-0 \[&\_pre\]:h-\[--container-height\] \[&\_pre\]:overflow-auto \[&\_pre\]:whitespace-break-spaces \[&\_pre\]:p-6 \[&\_pre\]:font-mono \[&\_pre\]:text-sm \[&\_pre\]:leading-relaxed"
/>
</TabsContent>
</Tabs>
)
}
As you can see, blocks are rendered using an iframe. An example URL is provided in the screenshot below.
Similarly, you can load other blocks by visiting their relevant URL.
Conclusion:
In these 5 parts, I studied the code used in building the Blocks page found on ui.shadcn.com/blocks.
On this side of codebase, I have seen some advanced Typescript patterns such as using Records and parsing objects with zod to make sure they meet certain set schema standards. My favourite was using ts-morph to perform some variable removing operations on the code picked from a file using AST API (that sounds cool, lol) just so the code presented to the “client” component requires what’s needed and nothing more than that.
Frankly speaking, it was not easy to read and understand this code. In my next adventure, I will use this momentum to understand how the shadcn-ui/ui’s CLI package is built and write articles about this CLI package. It will be interesting to find out what happens under the hood when you type npx shadcn-ui add button\
for example.
Get free courses inspired by the best practices used in open source.
About me:
Website: https://ramunarasinga.com/
Linkedin: https://www.linkedin.com/in/ramu-narasinga-189361128/
Github: https://github.com/Ramu-Narasinga
Email: ramu.narasinga@gmail.com
Learn the best practices used in open source.
Top comments (0)