Skip to content

Understanding the .slice() Method

What is Slice?

The .slice() method is one of the most powerful features in React Signify. It allows you to create a derived signify that focuses on just a specific part of your original state. Think of it like creating a "window" that only shows the data you care about.

tsx
const ssSlice = signify.slice(pickFunction);

Why is this useful?

  • 🚀 Performance: Your components only re-render when the specific data they need actually changes
  • 🎯 Focus: Each component gets exactly the data it needs, nothing more
  • 💡 Computed Values: You can create calculated values that automatically update when their dependencies change

The Problem Slice Solves

Before we dive into how slice works, let's understand the problem it solves:

tsx
// ❌ Problem: Without slice
const sAppState = signify({
  user: { name: "John", email: "john@example.com" },
  cart: [{ id: 1, name: "Item 1" }],
  theme: "light",
  notifications: [],
});

function UserName() {
  const appState = sAppState.use(); // Re-renders when ANYTHING changes!
  return <h1>{appState.user.name}</h1>; // Even when cart, theme, or notifications change
}

In this example, the UserName component will re-render whenever any part of sAppState changes, even though it only cares about the user's name.

tsx
// ✅ Solution: With slice
const ssUserName = sAppState.slice((state) => state.user.name);

function UserName() {
  const name = ssUserName.use(); // Only re-renders when user.name changes!
  return <h1>{name}</h1>;
}

Now the component only re-renders when the user's name actually changes!

Basic Usage Examples

1. Simple Property Access

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

const sUser = signify({
  id: 1,
  name: "John Doe",
  email: "john@example.com",
  profile: {
    age: 25,
    city: "New York",
  },
});

// Create slices for specific properties
const ssUserName = sUser.slice((user) => user.name);
const ssUserEmail = sUser.slice((user) => user.email);

function UserName() {
  const name = ssUserName.use(); // Only re-renders when name changes
  return <h1>Hello, {name}!</h1>;
}

function UserEmail() {
  const email = ssUserEmail.use(); // Only re-renders when email changes
  return <p>Contact: {email}</p>;
}

2. Nested Property Access

tsx
// Accessing deeply nested properties
const ssUserAge = sUser.slice((user) => user.profile.age);
const ssUserCity = sUser.slice((user) => user.profile.city);

function UserInfo() {
  const age = ssUserAge.use(); // Only re-renders when age changes
  const city = ssUserCity.use(); // Only re-renders when city changes

  return (
    <div>
      <p>Age: {age}</p>
      <p>City: {city}</p>
    </div>
  );
}

// Safe nested access with null checking
const ssUserBirthYear = sUser.slice((user) => {
  if (!user.profile?.age) return "Unknown";
  return new Date().getFullYear() - user.profile.age;
});

3. Creating Computed Values

One of the most powerful features of slice is creating computed values - values that are automatically calculated from your state and only recalculated when their dependencies change.

tsx
const sCart = signify([
  { id: 1, price: 100, quantity: 2 },
  { id: 2, price: 25, quantity: 1 }
]);

// Computed slices - automatically calculated
const ssItemCount = sCart.slice(items => items.length);
const ssTotal = sCart.slice(items => 
  items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);

function CartSummary() {
  const itemCount = ssItemCount.use(); // Re-renders when count changes
  const total = ssTotal.use();         // Re-renders when total changes
  
  return (
    <div>
      <p>Items: {itemCount}</p>
      <p>Total: ${total}</p>
    </div>
  );
}

What's happening here?

  • Each computed slice only recalculates when its specific dependencies change
  • Adding a new item triggers updates to both ssItemCount and ssTotal
  • Changing just the quantity updates ssTotal, but not ssItemCount
  • This selective updating is much more efficient than recalculating everything on every change

What You Can Do with Slices

A slice behaves very similarly to a regular signify, but with some important differences. Let's explore what's available and what's not.

✅ Available Methods and Properties

Since a slice is a derived value, you can read from it and watch it, but you cannot modify it directly:

tsx
const sUser = signify({ name: 'John', age: 25, city: 'New York' });
const ssUserName = sUser.slice(user => user.name);

// 1. Reading the current value
const currentName = ssUserName.value;           // 'John'

// 2. Using in React components (with reactivity)
const name = ssUserName.use();                  // Subscribe to changes

// 3. Watching for changes outside React
ssUserName.watch(name => {
  console.log('Name changed to:', name);
});

// 4. Rendering as HTML string
const nameHtml = ssUserName.html;               // '<span>John</span>'

// 5. Using wrapper components
<ssUserName.Wrap>
  {name => <div className="user-name">{name}</div>}
</ssUserName.Wrap>

<ssUserName.HardWrap>
  {name => <div className="user-name">{name}</div>}
</ssUserName.HardWrap>

// 6. Controlling when the slice updates
ssUserName.conditionRendering(name => name.length > 0); // Only update if name isn't empty

// 7. Pausing and resuming updates
ssUserName.stop();    // Temporarily stop receiving updates
ssUserName.resume();  // Resume updates

// 8. Development tools
<ssUserName.DevTool />  // Debug component for development

❌ What You Cannot Do with Slices

Since slices are derived from the original signify, you cannot modify them directly:

tsx
// ❌ Cannot set slice values directly
// ssUserName.set('Jane'); // Error! Slices are read-only

// ❌ Cannot reset slices
// ssUserName.reset(); // Error! Only the original signify can be reset

// ❌ Cannot configure slice updating conditions
// ssUserName.conditionUpdating(...); // Error! This is only for the source signify

To change a slice's value, you must change the original signify:

tsx
// ✅ Correct way: change the original signify
sUser.set((prev) => {
  prev.value.name = "Jane"; // This will automatically update ssUserName
});

// ✅ Or use the simpler syntax
sUser.set({ name: "Jane", age: 25, city: "New York" });

Why Slice Improves Performance

Understanding the performance benefits of slice is crucial for building efficient React applications. Let's explore the key advantages:

1. Prevents Unnecessary Re-renders

Without slice, components re-render whenever any part of the state changes, even if they don't care about those changes:

tsx
const sAppState = signify({
  user: { name: "John", email: "john@example.com" },
  cart: [{ id: 1, name: "Laptop", price: 1000 }],
  theme: "light",
  notifications: [],
  sidebar: { isOpen: false },
  modal: { isVisible: false },
});

// ❌ PROBLEM: Over-rendering
function UserGreeting() {
  const appState = sAppState.use(); // Subscribes to ALL changes!
  return <h1>Hello, {appState.user.name}!</h1>;
}

// This component will re-render when:
// - Cart items change
// - Theme changes
// - Notifications are added
// - Sidebar opens/closes
// - Modal opens/closes
// Even though it only needs the user's name!

With slice, you can surgically subscribe to only the data you need:

tsx
// ✅ SOLUTION: Precise subscriptions
const ssUserName = sAppState.slice((state) => state.user.name);

function UserGreeting() {
  const userName = ssUserName.use(); // Only subscribes to user.name changes!
  return <h1>Hello, {userName}!</h1>;
}

// This component now ONLY re-renders when user.name actually changes
// Changes to cart, theme, notifications, etc. are ignored

2. Caches Expensive Computations

Without slice, expensive calculations are performed on every render:

tsx
const sTransactions = signify([
  { amount: 100, type: "income" },
  { amount: 50, type: "expense" }
]);

// ❌ PROBLEM: Recalculating on every render
function FinancialSummary() {
  const transactions = sTransactions.use();
  
  // This expensive calculation runs on EVERY render!
  const total = transactions.reduce((sum, t) => 
    sum + (t.type === "income" ? t.amount : -t.amount), 0
  );

  return <div>Net: ${total}</div>;
}

With slice, the calculation is cached and only runs when data actually changes:

tsx
// ✅ SOLUTION: Cached computations
const ssTotal = sTransactions.slice(transactions => 
  // This calculation only runs when transactions actually change!
  transactions.reduce((sum, t) => 
    sum + (t.type === "income" ? t.amount : -t.amount), 0
  )
);

function FinancialSummary() {
  const total = ssTotal.use(); // Gets cached result!
  return <div>Net: ${total}</div>;
}

3. Performance Comparison

Here's what happens in a typical app:

tsx
// Without slice: Component subscribes to everything
const BadDashboard = () => {
  const appState = sAppState.use(); // 😱 Re-renders on ANY change
  return <div>User: {appState.user.name}</div>;
};

// With slice: Component subscribes precisely
const GoodDashboard = () => {
  const userName = ssUserName.use(); // 🚀 Re-renders only when needed
  return <div>User: {userName}</div>;
};

// Performance difference:
// - Bad: Re-renders 50+ times per minute in a typical app
// - Good: Re-renders only when user.name actually changes (maybe 1-2 times per session)

Real-world impact:

  • ✅ Smoother animations and interactions
  • ✅ Better battery life on mobile devices
  • ✅ Improved user experience, especially on slower devices
  • ✅ Easier debugging (fewer unnecessary renders to track)

Advanced Slice Patterns

Once you understand the basics of slice, you can use these advanced patterns to build more sophisticated applications.

1. Safe Conditional Slicing

When working with data that might be null or undefined, you can use slice to provide safe defaults:

tsx
const sUser = signify({
  user: null,
  permissions: []
});

// Safe slicing with fallback values
const ssUserName = sUser.slice(data => data.user?.name || "Guest");
const ssCanEdit = sUser.slice(data => data.permissions.includes("admin"));

function UserHeader() {
  const userName = ssUserName.use(); // Always safe, never crashes
  const canEdit = ssCanEdit.use();   // true/false based on permissions

  return (
    <div>
      <h1>Welcome, {userName}!</h1>
      {canEdit && <button>Edit Content</button>}
    </div>
  );
}

// When user logs in:
sUser.set(prev => {
  prev.value.user = { name: "John Doe" };
  prev.value.permissions = ["admin"];
});

2. Complex Object Transformations

Sometimes you need to combine data from multiple parts of your state into a new structure. Slice is perfect for this:

tsx
const sOrder = signify({
  items: [{ price: 100, quantity: 2 }, { price: 50, quantity: 1 }],
  customer: { name: "Sarah", isPremium: true },
  tax: 0.1
});

// Combine data from multiple parts into new structure
const ssOrderSummary = sOrder.slice(data => {
  const subtotal = data.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const tax = subtotal * data.tax;
  
  return {
    customer: data.customer.name,
    isPremium: data.customer.isPremium,
    subtotal,
    tax,
    total: subtotal + tax
  };
});

function OrderCard() {
  const summary = ssOrderSummary.use();
  
  return (
    <div>
      <h3>Order for {summary.customer}</h3>
      {summary.isPremium && <span>Premium Member</span>}
      <p>Subtotal: ${summary.subtotal}</p>
      <p>Tax: ${summary.tax}</p>
      <p><strong>Total: ${summary.total}</strong></p>
    </div>
  );
}

What makes this powerful:

  • The slice combines data from items, customer, and tax into one object
  • All calculations are cached - they only run when the source data changes
  • The component gets a clean, ready-to-use summary object
  • Adding a new item automatically recalculates the totals

3. Array Filtering and Sorting

One of the most common use cases for slice is efficiently filtering and sorting lists. Here's how to do it right:

tsx
const sTasks = signify([
  { id: 1, title: "Task A", completed: false, priority: "high" },
  { id: 2, title: "Task B", completed: true, priority: "low" }
]);

const sFilter = signify({ status: "all", priority: "all" });

// Step 1: Filter tasks
const ssFilteredTasks = signify.slice(() => {
  const tasks = sTasks.value;
  const filter = sFilter.value;
  
  return tasks.filter(task => {
    const statusMatch = filter.status === "all" || 
      (filter.status === "pending" && !task.completed) ||
      (filter.status === "completed" && task.completed);
    const priorityMatch = filter.priority === "all" || task.priority === filter.priority;
    return statusMatch && priorityMatch;
  });
});

// Step 2: Sort filtered tasks
const ssSortedTasks = ssFilteredTasks.slice(tasks => 
  [...tasks].sort((a, b) => a.title.localeCompare(b.title))
);

function TaskList() {
  const tasks = ssSortedTasks.use(); // Only updates when filtered/sorted results change
  
  return (
    <div>
      <div>
        <select 
          value={sFilter.value.status} 
          onChange={(e) => sFilter.set(prev => { prev.value.status = e.target.value; })}
        >
          <option value="all">All</option>
          <option value="pending">Pending</option>
          <option value="completed">Completed</option>
        </select>
      </div>
      
      <div>
        {tasks.map(task => (
          <div key={task.id}>
            <span>{task.title} - {task.priority}</span>
            <span>{task.completed ? "✓" : "○"}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

Performance Benefits of This Approach:

  • Filtering only happens when tasks or filters change
  • Sorting only happens when filtered results or sort options change
  • Statistics only recalculate when the task list changes
  • Unique assignees only recalculate when assignees change
  • ✅ UI components only re-render when their specific data changes

Traditional approach problems:

  • ❌ All calculations happen on every render
  • ❌ All components re-render when any filter changes
  • ❌ No caching of expensive operations
  • ❌ Poor performance with large lists

4. Slice Chaining for Complex Transformations

You can create slices from other slices to build complex data transformations. This creates a dependency chain where each slice only updates when its specific input changes.

tsx
const sEmployees = signify([
  { id: 1, name: "Alice", department: "Engineering", salary: 100000, active: true },
  { id: 2, name: "Bob", department: "Marketing", salary: 80000, active: true },
  { id: 3, name: "Charlie", department: "Engineering", salary: 90000, active: false }
]);

// Step 1: Get active employees only
const ssActiveEmployees = sEmployees.slice(employees => 
  employees.filter(emp => emp.active)
);

// Step 2: Filter by department (chaining from previous slice)
const ssEngineers = ssActiveEmployees.slice(employees =>
  employees.filter(emp => emp.department === "Engineering")
);

// Step 3: Calculate team stats (chaining again)
const ssEngStats = ssEngineers.slice(engineers => ({
  count: engineers.length,
  avgSalary: engineers.reduce((sum, eng) => sum + eng.salary, 0) / engineers.length
}));

function EngTeamDashboard() {
  const activeEmployees = ssActiveEmployees.use();
  const engStats = ssEngStats.use();
  
  return (
    <div>
      <h2>Active Employees: {activeEmployees.length}</h2>
      <h3>Engineering Team</h3>
      <p>Engineers: {engStats.count}</p>
      <p>Average Salary: ${engStats.avgSalary.toLocaleString()}</p>
    </div>
  );
}

How Slice Chaining Works:

  1. ssActiveEmployees → Only updates when employee active status changes
  2. ssEngineers → Only updates when active employees change AND some are engineering
  3. ssEngStats → Only updates when the engineering team membership or data changes

Performance Benefits:

  • ✅ Each slice only recalculates when its specific inputs change
  • ✅ Adding a non-engineering employee doesn't trigger engineering stats recalculation
  • ✅ Changing an inactive employee's data doesn't affect any active employee calculations
  • ✅ Complex dependency chains are automatically managed and optimized

Using Slice with Forms

Forms are a perfect use case for slice because different parts of your form often need different pieces of data, and you want validation to be efficient.

1. Smart Form Field Management

tsx
const sForm = signify({
  name: '',
  email: '',
  password: ''
});

// Field-specific slices
const ssName = sForm.slice(form => form.name);
const ssEmail = sForm.slice(form => form.email);

// Validation slices
const ssEmailValid = sForm.slice(form => 
  form.email.includes('@') && form.email.length > 5
);

function ContactForm() {
  const name = ssName.use();
  const email = ssEmail.use();
  const emailValid = ssEmailValid.use();
  
  return (
    <form>
      <input
        value={name}
        onChange={(e) => sForm.set(prev => { prev.value.name = e.target.value; })}
      />
      <input
        value={email}
        className={emailValid ? 'valid' : 'invalid'}
        onChange={(e) => sForm.set(prev => { prev.value.email = e.target.value; })}
      />
    </form>
  );
}

Best Practices for Using Slice

Understanding these best practices will help you get the most out of slice and avoid common pitfalls:

✅ DO: Use Slice for These Scenarios

tsx
// ✅ Computed values that depend on multiple pieces of data
const ssOrderTotal = sCart.slice(cart => 
  cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);

// ✅ Nested property access to avoid deep subscriptions
const ssUserName = sUserData.slice(data => data.user?.name || 'Guest');

// ✅ Expensive transformations that should be cached
const ssSearchResults = sProductList.slice(products => 
  products.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()))
);

❌ DON'T: Avoid These Anti-patterns

tsx
// ❌ Don't slice simple values unnecessarily
const ssMessage = sSimpleString.slice(msg => msg); // Pointless wrapper

// ❌ Don't perform side effects in slice functions
const ssBadSlice = sData.slice(data => {
  console.log(data); // Side effect!
  localStorage.setItem('data', JSON.stringify(data)); // Side effect!
  return data.processed;
});

// ❌ Don't create slices inside component render
function BadComponent({ userId }) {
  // This creates a new slice on every render!
  const ssDynamicSlice = sUsers.slice(users => users.find(u => u.id === userId));
  return <div>{ssDynamicSlice.use()?.name}</div>;
}

🎯 When to Choose Slice vs Direct Usage

ScenarioUse SliceUse Direct
Simple property accessOnly if nested deeply✅ Direct usage
Computed/derived values✅ Always use slice
Expensive calculations✅ Always use slice
Multiple components need same data✅ Use slice
Filtering/sorting arrays✅ Use slice
Form validation✅ Use slice
Direct input binding✅ Direct usage
Simple boolean flags✅ Direct usage

TypeScript Support

Slice works seamlessly with TypeScript, providing full type safety and automatic type inference:

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

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

// ✅ Type-safe slices with automatic inference
const ssUserName = sUser.slice(user => user?.name || 'Guest'); 
// Type: string

const ssUserAge = sUser.slice(user => user?.profile.age || 0);
// Type: number

const ssUserTheme = sUser.slice(user => user?.profile.preferences.theme || 'light');
// Type: 'light' | 'dark'

TypeScript Benefits:

  • ✅ Automatic type inference for slice return values
  • ✅ Compile-time error detection
  • ✅ IntelliSense support in editors
  • ✅ Refactoring safety across your codebase

Summary

The .slice() method is a powerful tool for building efficient, maintainable React applications with React Signify.

Key Concepts

  1. Derived State: Slices create derived values that automatically update when their source data changes
  2. Selective Updates: Components only re-render when their specific slice data changes, not when unrelated data changes
  3. Caching: Expensive computations are cached and only recalculated when necessary
  4. Type Safety: Full TypeScript support with automatic type inference

When to Use Slice

Perfect for:

  • Computed/derived values
  • Nested property access
  • Expensive transformations
  • Filtering and sorting arrays
  • Form validation
  • Complex object transformations
  • Performance optimization

Avoid for:

  • Simple property access (unless deeply nested)
  • Direct input value binding
  • Simple boolean flags
  • Values that don't need computation

Performance Benefits

  • 🚀 Prevents over-rendering: Components only update when relevant data changes
  • 💾 Caches computations: Expensive calculations run only when needed
  • 🎯 Precise subscriptions: Subscribe to exactly the data you need
  • 🔧 Optimizes automatically: No manual optimization required

Best Practices Recap

  1. Create slices outside components - Define them at module level
  2. Keep slice functions pure - No side effects, always return new data
  3. Handle edge cases - Use optional chaining and provide fallbacks
  4. Use meaningful names - Make slice purpose clear from the name
  5. Chain slices for complex logic - Build dependencies between slices
  6. Test slice behavior - Verify updates happen only when expected

Real-World Impact

Using slice properly leads to:

  • ✅ Smoother user interfaces
  • ✅ Better performance on slower devices
  • ✅ Easier debugging and maintenance
  • ✅ More predictable component behavior
  • ✅ Cleaner, more organized code

💡 Next Steps: Now that you understand slice, explore other React Signify features like .watch() for side effects, .reset() for state management, and .wrap() for advanced component patterns.

Remember: Slice is your tool for creating efficient, focused components that only care about the data they actually need!

Released under the MIT License.