This content originally appeared on DEV Community and was authored by Yuki Teraoka
The Problem with Skeleton Screens
When using React Suspense or client-side data fetching, we often want to show a skeleton as a loading fallback. However, we frequently end up giving up on proper skeleton implementations because:
- Writing skeleton JSX is time-consuming and duplicates component structure
- Hard to keep skeleton JSX in sync with component changes
- Components throw TypeErrors when props are missing
- Maintenance burden grows with each component
The Solution: withStencil
withStencil is a HOC that wraps any React component and automatically generates a skeleton version. No need to write duplicate JSX or add conditional branches.
Installation
npm install react-stencilize
Basic Usage
Note: For styling skeletons with Tailwind CSS v4, check out Design Skeletons by Character Count
![]()
Design Skeletons by Character Count — Tailwind CSS v4
Yuki Teraoka ・ Nov 9
#tailwindcss #css #frontend
Let's create a UserView component to render user information:
type User = { name: string; bio?: string }
const UserView = (props: { user: User }) => {
return (
<div>
<h4>{props.user.name}</h4>
<p>{props.user.bio}</p>
</div>
)
}
Without props, this component would throw a TypeError. But with withStencil:
import { withStencil } from 'react-stencilize'
import { UserView } from './components/user'
const StencilUserView = withStencil(UserView)
function App() {
return (
<>
{/* Normal rendering with props */}
<UserView user={{ name: 'John Doe', bio: 'Software Engineer at Example Corp' }} />
{/* Skeleton rendering without props */}
<StencilUserView />
</>
)
}
StencilUserView preserves the UI structure while replacing only the text and dynamic content with empty values - perfect for skeleton display.
How It Works
withStencil uses three key mechanisms:
1. Deep Safe Proxy
- Wraps props with a Proxy that returns safe dummy values at any depth
- Prevents crashes on any property access, even deeply nested ones
- No TypeErrors when props are missing
2. Recursive Element Transformation
- Traverses component output recursively
- Converts to skeleton-friendly format while preserving structure
3. Smart Transformation Rules
- Strings/numbers → empty strings
- Arrays → recursively processed per element
- React elements → structure preserved, children recursively transformed
- Objects → safely wrapped with Proxy
Using with React Suspense
withStencil shines when used as a Suspense fallback:
import { use, Suspense } from 'react'
import { withStencil } from 'react-stencilize'
type User = { name: string; bio?: string }
const UserView = (props: { user: User }) => {
return (
<div>
<h4>{props.user.name}</h4>
<p>{props.user.bio}</p>
</div>
)
}
const UserCard = (props: { user: Promise<User> }) => {
const user = use(props.user)
return <UserView user={user} />
}
// Generate skeleton version
const StencilUserView = withStencil(UserView)
function App() {
const userPromise = fetchUser() // Promise<User>
return (
<Suspense fallback={<StencilUserView />}>
<UserCard user={userPromise} />
</Suspense>
)
}
Key Benefits
Since StencilUserView is derived from UserView:
- No layout mismatch between skeleton and actual content
- Automatic synchronization - component changes reflect in skeleton
- Smooth transitions from loading to loaded states
- Zero duplication - single source of truth
Complex Component Example
withStencil works with complex nested components too:
type Post = {
title: string
content: string
author: { name: string; avatar: string }
tags: string[]
}
const PostView = (props: { post: Post }) => {
return (
<article>
<h1>{props.post.title}</h1>
<div>
<img src={props.post.author.avatar} alt={props.post.author.name} />
<span>{props.post.author.name}</span>
</div>
<p>{props.post.content}</p>
<ul>
{props.post.tags.map(tag => <li key={tag}>{tag}</li>)}
</ul>
</article>
)
}
const StencilPostView = withStencil(PostView)
// Use as Suspense fallback
<Suspense fallback={<StencilPostView />}>
<PostView post={post} />
</Suspense>
Even with deeply nested properties, arrays, and complex structures, withStencil safely renders the skeleton version without any TypeErrors.
Why withStencil?
Traditional Approach Problems
// ❌ Duplicate JSX maintenance burden
const UserView = ({ user, skeleton }) => {
if (skeleton) {
return (
<div>
<h4 className="skeleton"></h4>
<p className="skeleton"></p>
</div>
)
}
return (
<div>
<h4>{user.name}</h4>
<p>{user.bio}</p>
</div>
)
}
withStencil Approach
// ✅ Single source of truth, automatic skeleton
const UserView = ({ user }) => {
return (
<div>
<h4>{user.name}</h4>
<p>{user.bio}</p>
</div>
)
}
const StencilUserView = withStencil(UserView)
Summary
- withStencil HOC automatically generates skeleton versions of React components
- No duplicate JSX - changes to components automatically reflect in skeletons
- Deep safe Proxy prevents TypeErrors when props are missing
- Perfect for Suspense fallback - zero layout mismatch
- Works with complex components - nested objects, arrays, any structure
Stop writing skeleton components twice. Share the same component for both normal rendering and skeleton display with withStencil.
Try it in your projects: react-stencilize
This content originally appeared on DEV Community and was authored by Yuki Teraoka
Yuki Teraoka | Sciencx (2025-11-20T11:40:05+00:00) withStencil – Stop Writing Skeleton Components Twice. Retrieved from https://www.scien.cx/2025/11/20/withstencil-stop-writing-skeleton-components-twice/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.
