Skip to content

Understanding the .Wrap Component

What is the Wrap Component?

The .Wrap component is a special React component that comes with every Signify instance. It uses the render prop pattern (also known as "function as children") to give you access to the current state value and automatically re-render when that value changes.

Think of it as a "window" into your Signify state that you can place anywhere in your component tree.

Basic Concept

tsx
<signify.Wrap>
  {value => <div>{value}</div>}
</signify.Wrap>

Here's what happens:

  1. Wrap subscribes to the Signify state
  2. Wrap calls your function with the current value
  3. Your function returns JSX to render
  4. When state changes, Wrap automatically calls your function again with the new value

Step-by-Step Learning

Step 1: Your First Wrap Component

Let's start with the simplest possible example:

tsx
import { signify } from 'react-signify';

// Create a Signify state with initial value of 0
const sCount = signify(0);

function MyFirstWrap() {
  return (
    <sCount.Wrap>
      {count => <div>The count is: {count}</div>}
    </sCount.Wrap>
  );
}

What's happening here:

  • sCount.Wrap creates a component that "watches" the count value
  • The function {count => <div>The count is: {count}</div>} is called every time the count changes
  • The count parameter contains the current value from sCount

Step 2: Making It Interactive

Now let's add a button to change the value:

tsx
function InteractiveCounter() {
  return (
    <div>
      <h1>My Counter</h1>
      <sCount.Wrap>
        {count => (
          <div>
            <p>Current count: {count}</p>
            <button onClick={() => sCount.set(count + 1)}>
              Click to increment
            </button>
          </div>
        )}
      </sCount.Wrap>
    </div>
  );
}

Key insight: When you click the button, sCount.set() updates the state, which automatically triggers the Wrap to re-render with the new value!

Step 3: Working with Complex Data

Wrap isn't limited to simple values like numbers. It works great with objects too:

tsx
const sUser = signify({
  name: 'John Doe',
  email: 'john@example.com',
  avatar: 'https://example.com/avatar.jpg'
});

function UserProfile() {
  return (
    <div className="user-profile">
      <sUser.Wrap>
        {user => (
          <div className="user-card">
            <img src={user.avatar} alt={`${user.name}'s avatar`} />
            <h2>{user.name}</h2>
            <p>{user.email}</p>
            <button onClick={() => sUser.set(prev => {
              prev.value.name = 'Jane Doe';
            })}>
              Change Name
            </button>
          </div>
        )}
      </sUser.Wrap>
    </div>
  );
}

What's new here:

  • The user parameter is now an object with multiple properties
  • You can access any property: user.name, user.email, user.avatar
  • When any property changes, the entire Wrap re-renders automatically

Step 4: Understanding Nested Wraps

Sometimes you need data from multiple Signify states. You can nest Wrap components:

tsx
const sFirstName = signify('John');
const sLastName = signify('Doe');
const sAge = signify(25);

function UserInfo() {
  return (
    <div>
      <sFirstName.Wrap>
        {firstName => (
          <sLastName.Wrap>
            {lastName => (
              <sAge.Wrap>
                {age => (
                  <div>
                    <h1>{firstName} {lastName}</h1>
                    <p>Age: {age}</p>
                    <button onClick={() => sAge.set(age + 1)}>
                      Happy Birthday!
                    </button>
                  </div>
                )}
              </sAge.Wrap>
            )}
          </sLastName.Wrap>
        )}
      </sFirstName.Wrap>
    </div>
  );
}

Important note: Nested Wraps work but can get hard to read. For multiple simple values, consider using .use() instead (we'll compare them later).

When to Use .Wrap vs .use()

Now that you understand how Wrap works, let's see when to use it versus the .use() hook.

The .use() Approach

tsx
function UserProfile() {
  const user = sUser.use();
  
  return (
    <div className="user-card">
      <img src={user.avatar} alt={`${user.name}'s avatar`} />
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

The .Wrap Approach

tsx
function UserProfile() {
  return (
    <div className="user-profile">
      <sUser.Wrap>
        {user => (
          <div className="user-card">
            <img src={user.avatar} alt={`${user.name}'s avatar`} />
            <h2>{user.name}</h2>
            <p>{user.email}</p>
          </div>
        )}
      </sUser.Wrap>
    </div>
  );
}

Quick Comparison

Aspect.use().Wrap
Readability✅ Simpler, more familiar⚠️ Can get complex when nested
Performance⚠️ Entire component re-renders✅ Only Wrap content re-renders
Multiple states✅ Easy to use multiple⚠️ Requires nesting
Conditional logic✅ Use normal if/else⚠️ Must be inside render function
Testing✅ Easier to test⚠️ More complex testing setup

When to Choose Each

Use .use() when:

  • You need multiple Signify values
  • You have complex conditional logic
  • You prefer a hook-like API
  • Your component is simple

Use .Wrap when:

  • You want better performance isolation
  • You're building reusable render prop components
  • You only need one Signify value
  • You want to avoid parent re-renders

Advanced Patterns

Once you're comfortable with the basics, here are some powerful patterns you can use with Wrap.

Pattern 1: Conditional Rendering

One of the most common patterns is showing different content based on the state value:

tsx
const sUser = signify(null); // null means no user logged in

function AuthenticatedContent() {
  return (
    <sUser.Wrap>
      {user => (
        user ? (
          // User is logged in
          <div>
            <h1>Welcome back, {user.name}!</h1>
            <UserDashboard user={user} />
          </div>
        ) : (
          // No user logged in
          <div>
            <h1>Please log in</h1>
            <LoginForm />
          </div>
        )
      )}
    </sUser.Wrap>
  );
}

Why this is useful:

  • The entire component switches between login and dashboard automatically
  • When sUser.set(userObject) is called, it instantly shows the dashboard
  • When sUser.set(null) is called, it instantly shows the login form

Pattern 2: Data Transformation and Calculations

You can perform calculations directly in the render function:

tsx
const sProducts = signify([
  { id: 1, name: 'Laptop', price: 1000 },
  { id: 2, name: 'Mouse', price: 25 },
  { id: 3, name: 'Keyboard', price: 75 }
]);

function ProductSummary() {
  return (
    <div>
      <h2>Product Summary</h2>
      <sProducts.Wrap>
        {products => {
          // Calculate totals from the products array
          const totalProducts = products.length;
          const totalValue = products.reduce((sum, p) => sum + p.price, 0);
          const averagePrice = totalValue / totalProducts;
          
          return (
            <div>
              <p>Total products: {totalProducts}</p>
              <p>Total value: ${totalValue}</p>
              <p>Average price: ${averagePrice.toFixed(2)}</p>
              
              <h3>Product List:</h3>
              <ul>
                {products.map(product => (
                  <li key={product.id}>
                    {product.name} - ${product.price}
                  </li>
                ))}
              </ul>
            </div>
          );
        }}
      </sProducts.Wrap>
    </div>
  );
}

Key point: All calculations update automatically when the products array changes!

Pattern 3: Loading States

A very practical pattern is handling loading, error, and success states:

tsx
const sApiState = signify({
  data: null,
  loading: false,
  error: null
});

// Helper function to simulate API call
const loadUserData = async () => {
  sApiState.set(prev => { prev.value.loading = true; prev.value.error = null; });
  
  try {
    const response = await fetch('/api/user');
    const data = await response.json();
    sApiState.set(prev => { 
      prev.value.data = data; 
      prev.value.loading = false; 
    });
  } catch (error) {
    sApiState.set(prev => { 
      prev.value.error = error.message; 
      prev.value.loading = false; 
    });
  }
};

function UserDataDisplay() {
  return (
    <div>
      <h2>User Information</h2>
      <sApiState.Wrap>
        {state => {
          // Loading state
          if (state.loading) {
            return (
              <div className="loading">
                <p>Loading user data...</p>
              </div>
            );
          }
          
          // Error state
          if (state.error) {
            return (
              <div className="error">
                <p>Failed to load data: {state.error}</p>
                <button onClick={loadUserData}>
                  Try Again
                </button>
              </div>
            );
          }
          
          // Success state
          if (state.data) {
            return (
              <div className="success">
                <h3>Welcome, {state.data.name}!</h3>
                <p>Email: {state.data.email}</p>
              </div>
            );
          }
          
          // Initial state
          return (
            <div className="initial">
              <p>Click to load your data</p>
              <button onClick={loadUserData}>
                Load My Data
              </button>
            </div>
          );
        }}
      </sApiState.Wrap>
    </div>
  );
}

This pattern is powerful because:

  • All state transitions happen automatically
  • The UI always reflects the current state
  • You handle all possible states in one place

Performance Understanding

Why Wrap Can Be More Performant

Understanding when Wrap performs better than .use() helps you make better architectural decisions.

Parent Re-render Problem

tsx
function ParentComponent() {
  const [parentState, setParentState] = useState(0);
  
  return (
    <div>
      <button onClick={() => setParentState(s => s + 1)}>
        Update Parent: {parentState}
      </button>
      
      {/* ❌ Problem: This will re-render when parent re-renders */}
      <ExpensiveUserProfile />
    </div>
  );
}

function ExpensiveUserProfile() {
  const user = sUser.use(); // This causes entire component to re-render
  
  return (
    <div className="expensive-component">
      {/* Imagine this is very expensive to render */}
      <ComplexChart data={user.data} />
      <HeavyCalculations user={user} />
    </div>
  );
}

Wrap Solution

tsx
function ParentComponent() {
  const [parentState, setParentState] = useState(0);
  
  return (
    <div>
      <button onClick={() => setParentState(s => s + 1)}>
        Update Parent: {parentState}
      </button>
      
      {/* ✅ Better: Only the Wrap content re-renders */}
      <UserProfileWrapper />
    </div>
  );
}

function UserProfileWrapper() {
  return (
    <sUser.Wrap>
      {user => (
        <div className="expensive-component">
          <ComplexChart data={user.data} />
          <HeavyCalculations user={user} />
        </div>
      )}
    </sUser.Wrap>
  );
}

Key insight: The wrapper component itself doesn't re-render when the parent changes, only when sUser changes.

Common Pitfalls and Best Practices

❌ Avoid: Deep Nesting

tsx
// ❌ Don't do this - it's hard to read and maintain
<sUser.Wrap>
  {user => (
    <sSettings.Wrap>
      {settings => (
        <sNotifications.Wrap>
          {notifications => (
            <sTheme.Wrap>
              {theme => (
                <div className={theme}>
                  <h1>{user.name}</h1>
                  <p>Notifications: {notifications.length}</p>
                  <p>Language: {settings.language}</p>
                </div>
              )}
            </sTheme.Wrap>
          )}
        </sNotifications.Wrap>
      )}
    </sSettings.Wrap>
  )}
</sUser.Wrap>

✅ Better: Use .use() for Multiple Values

tsx
// ✅ Much cleaner for multiple values
function UserDashboard() {
  const user = sUser.use();
  const settings = sSettings.use();
  const notifications = sNotifications.use();
  const theme = sTheme.use();
  
  return (
    <div className={theme}>
      <h1>{user.name}</h1>
      <p>Notifications: {notifications.length}</p>
      <p>Language: {settings.language}</p>
    </div>
  );
}

❌ Avoid: Expensive Operations in Render Function

tsx
// ❌ Don't do heavy calculations every render
<sLargeDataset.Wrap>
  {data => {
    // This runs on every render - bad performance!
    const expensiveResult = data.reduce((acc, item) => {
      return performComplexCalculation(acc, item);
    }, {});
    
    return <div>{JSON.stringify(expensiveResult)}</div>;
  }}
</sLargeDataset.Wrap>

✅ Better: Use .slice() for Computed Values

tsx
// ✅ Calculate once, subscribe to changes
const sExpensiveResult = sLargeDataset.slice(data => 
  data.reduce((acc, item) => performComplexCalculation(acc, item), {})
);

function DataDisplay() {
  return (
    <sExpensiveResult.Wrap>
      {result => <div>{JSON.stringify(result)}</div>}
    </sExpensiveResult.Wrap>
  );
}
## TypeScript Support

Wrap components work seamlessly with TypeScript and provide full type safety:

```tsx
interface User {
  id: number;
  name: string;
  preferences: {
    theme: 'light' | 'dark';
    language: string;
  };
}

const sUser = signify<User | null>(null);

function TypeSafeUserProfile() {
  return (
    <div>
      <sUser.Wrap>
        {(user) => ( // user: User | null (automatically inferred)
          user ? (
            <div>
              <h1>{user.name}</h1>           {/* ✅ Type-safe */}
              <p>Theme: {user.preferences.theme}</p>  {/* ✅ IntelliSense works */}
              <p>Language: {user.preferences.language}</p>
            </div>
          ) : (
            <div>No user logged in</div>
          )
        )}
      </sUser.Wrap>
    </div>
  );
}

TypeScript benefits:

  • Automatic type inference for the value parameter
  • Full IntelliSense support in your editor
  • Compile-time error checking
  • Refactoring safety

Quick Reference

Basic Syntax

tsx
<signify.Wrap>
  {value => <div>{value}</div>}
</signify.Wrap>

When to Use .Wrap

Good for:

  • Single Signify state access
  • Render prop patterns
  • Performance isolation from parent re-renders
  • Building reusable components

Avoid when:

  • You need multiple Signify states (use .use() instead)
  • You have complex conditional logic before rendering
  • The component is very simple (.use() might be cleaner)

Performance Tips

  • Avoid deep nesting of multiple Wraps
  • Don't do expensive calculations in the render function
  • Use .slice() for computed values
  • Consider .HardWrap for even better performance isolation

Summary

The .Wrap component is a powerful tool in the React Signify toolkit that provides:

Key Benefits

  1. Render Prop Pattern: Clean function-as-children syntax
  2. Performance Isolation: Only re-renders when Signify state changes, not parent
  3. Reactive UI: Automatically updates when state changes
  4. Type Safety: Full TypeScript support with automatic inference

When to Choose Wrap vs Use

Choose .Wrap when:

  • You need performance isolation from parent re-renders
  • You're building reusable render prop components
  • You want to encapsulate reactive logic
  • You only need one Signify state

Choose .use() when:

  • You need multiple Signify states
  • You have complex conditional logic
  • The component is simple and straightforward
  • You prefer hook-style APIs

Final Tips

  1. Start Simple: Begin with basic .Wrap usage and gradually explore advanced patterns
  2. Avoid Deep Nesting: Use .use() for multiple states instead of nesting Wraps
  3. Performance Matters: Use .slice() for computed values and .HardWrap for heavy components
  4. Type Safety: Leverage TypeScript for better development experience

The .Wrap component bridges the gap between imperative state management and declarative React patterns, giving you the best of both worlds!


💡 Next Steps: Try the .HardWrap component for even better performance, or explore .slice() for computed values.

Released under the MIT License.