π§© State Selection β
Sometimes, you don't need the entire stateβjust a specific part of it. With Stunk, you can efficiently select and react to specific properties without unnecessary re-renders using the powerful select
function.
Basic Selection β
Use select
to create readonly chunks derived from an existing chunk:
import { chunk, select } from "stunk";
const userChunk = chunk({
name: "Olamide",
age: 30,
email: "olamide@example.com",
});
// Select specific properties (readonly)
const nameChunk = select(userChunk, (state) => state.name);
const ageChunk = select(userChunk, (state) => state.age);
// Subscribe to receive updates
nameChunk.subscribe((name) => console.log("Name changed:", name));
ageChunk.subscribe((age) => console.log("Age changed:", age));
Readonly Nature of Selectors β
Selected chunks are readonly - you cannot modify them directly:
// β This will throw an error! nameChunk is readonly.
nameChunk.set("Fola");
// β Reset is also not allowed on selectors
nameChunk.reset();
// β
Update the source chunk instead
userChunk.set((state) => ({ ...state, name: "Fola" }));
// This will automatically update nameChunk and notify its subscribers
Selecting Nested Properties β
Here's how to efficiently select and react to deeply nested properties:
const userChunk = chunk({
profile: {
name: "Olamide",
age: 30,
email: "olamide@example.com",
},
settings: {
theme: "dark",
notifications: true,
},
preferences: {
language: "en",
timezone: "UTC",
}
});
// Select deeply nested properties (readonly)
const nameChunk = select(userChunk, (state) => state.profile.name);
const themeChunk = select(userChunk, (state) => state.settings.theme);
const languageChunk = select(userChunk, (state) => state.preferences.language);
// Subscribe to selected properties
nameChunk.subscribe((name) => console.log("Name changed:", name));
themeChunk.subscribe((theme) => console.log("Theme changed:", theme));
Optimized Updates β
Selectors only trigger updates when their specific selected value changes:
// Update the name - only nameChunk subscribers will be notified
userChunk.set((state) => ({
...state,
profile: { ...state.profile, name: "David" }
}));
// Logs: "Name changed: David"
// Update the theme - only themeChunk subscribers will be notified
userChunk.set((state) => ({
...state,
settings: { ...state.settings, theme: "light" }
}));
// Logs: "Theme changed: light"
// Update email - no selector subscribers are notified (optimization!)
userChunk.set((state) => ({
...state,
profile: { ...state.profile, email: "new@example.com" }
}));
// No logs from selectors - they weren't affected
Advanced Selection Options β
Shallow Equality Optimization β
For complex objects or arrays, you can enable shallow equality checking to prevent unnecessary updates:
const todosChunk = chunk([
{ id: 1, text: "Learn Stunk", completed: false },
{ id: 2, text: "Build app", completed: true },
]);
// Select completed todos with shallow equality checking
const completedTodos = select(
todosChunk,
(todos) => todos.filter(todo => todo.completed),
{ useShallowEqual: true }
);
completedTodos.subscribe((completed) => {
console.log("Completed todos:", completed.length);
});
// This won't trigger the selector if the filtered result is the same
todosChunk.set(current => [...current]); // Array reference changes but content is same
Chaining Selectors β
You can chain selectors to create more focused selections:
const appState = chunk({
user: {
profile: { name: "Olamide", settings: { theme: "dark" } },
posts: [{ id: 1, title: "Hello World" }]
}
});
// First level selection
const userChunk = select(appState, (state) => state.user);
// Second level selection using the derive method (which uses select internally)
const userProfileChunk = userChunk.derive((user) => user.profile);
const userPostsChunk = userChunk.derive((user) => user.posts);
// Third level selection
const themeChunk = userProfileChunk.derive((profile) => profile.settings.theme);
Complex Transformations β
Selectors can perform complex transformations on your data:
const productsChunk = chunk([
{ id: 1, name: "Laptop", price: 1000, category: "electronics" },
{ id: 2, name: "Book", price: 20, category: "books" },
{ id: 3, name: "Phone", price: 800, category: "electronics" },
]);
// Select and transform data
const expensiveElectronics = select(
productsChunk,
(products) => products
.filter(p => p.category === "electronics" && p.price > 500)
.map(p => ({ ...p, displayName: `${p.name} ($${p.price})` }))
);
const productStats = select(productsChunk, (products) => ({
total: products.length,
categories: [...new Set(products.map(p => p.category))],
averagePrice: products.reduce((sum, p) => sum + p.price, 0) / products.length,
mostExpensive: products.reduce((max, p) => p.price > max.price ? p : max)
}));
Memory Management β
Selectors automatically clean up when destroyed:
const sourceChunk = chunk({ value: 1 });
const selectedChunk = select(sourceChunk, (state) => state.value);
// When you destroy a selector, it unsubscribes from the source
selectedChunk.destroy();
// When you destroy the source, all its selectors are also cleaned up
sourceChunk.destroy();
Performance Best Practices β
- Use shallow equality for objects: Enable
useShallowEqual
when selecting complex objects or arrays - Keep selectors focused: Select only what you need to minimize unnecessary computations
- Avoid complex transformations: For heavy computations, consider using
computed
instead - Clean up selectors: Always destroy selectors when they're no longer needed
// β
Good - focused selection
const userName = select(userChunk, (user) => user.name);
// β Avoid - selecting entire objects when you only need part
const entireUser = select(appState, (state) => state.user); // If you only need the name
// β
Good - using shallow equality for arrays
const filteredItems = select(
itemsChunk,
(items) => items.filter(item => item.active),
{ useShallowEqual: true }
);
Error Handling β
Selectors include built-in error handling:
const dataChunk = chunk({ user: { name: "John" } });
// Safe selection with optional chaining
const safeNameChunk = select(dataChunk, (data) => data.user?.name || "Unknown");
// The selector will handle cases where the structure changes
dataChunk.set({ user: null }); // safeNameChunk will emit "Unknown"
Why Use State Selection? β
β
Optimized Performance β Only updates when the selected value actually changes
β
Avoid Unnecessary Renders β Components subscribed to a selection won't re-render if other properties change
β
Readonly Safety β Prevents accidental state modifications outside the main chunk
β
Memory Efficient β Automatic cleanup and subscription management
β
Type Safe β Full TypeScript support with proper type inference
Common Patterns β
Form Field Selection β
const formChunk = chunk({
name: "", email: "", age: 0, errors: {}
});
const nameField = select(formChunk, (form) => form.name);
const emailField = select(formChunk, (form) => form.email);
const formErrors = select(formChunk, (form) => form.errors);
UI State Selection β
const uiState = chunk({
sidebar: { open: false, width: 250 },
modal: { visible: false, content: null },
theme: "light"
});
const sidebarOpen = select(uiState, (state) => state.sidebar.open);
const modalVisible = select(uiState, (state) => state.modal.visible);
const currentTheme = select(uiState, (state) => state.theme);
Want to update multiple chunks efficiently while notifying subscribers just once? Let's dive into Batch Updates next! π