DEV Community

Yuki Teraoka
Yuki Teraoka

Posted on

withStencil - Stop Writing Skeleton Components Twice

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
Enter fullscreen mode Exit fullscreen mode

Basic Usage

Note: For styling skeletons with Tailwind CSS v4, check out Design Skeletons by Character Count


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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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 />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Fallback sample

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)