@winglet/react-utils
Version:
React utility library providing custom hooks, higher-order components (HOCs), and utility functions to enhance React application development with improved reusability and functionality
152 lines (151 loc) • 5.27 kB
TypeScript
import type { Dictionary } from '../@aileron/declare';
/**
* Maintains referential stability for object props by returning the same reference when contents are identical.
*
* This hook performs shallow equality comparison and returns the previous object reference
* if all properties and values remain the same. It's essential for preventing unnecessary
* re-renders in memoized components when object props are created inline or computed dynamically.
*
* ### Performance Problem it Solves
* React creates new object references on every render for:
* - Inline object literals: `<Component config={{ theme: 'dark' }} />`
* - Spread operations: `<Component {...restProps} />`
* - Computed objects: `<Component data={{ ...state, computed: value }} />`
*
* Even when contents are identical, new references break memoization and cause re-renders.
*
* ### Use Cases
* - **Rest Props Optimization**: Stabilize `{...restProps}` in component APIs
* - **Context Value Stability**: Prevent context consumer re-renders
* - **Dynamic Prop Objects**: Maintain stable references for computed configurations
* - **HOC Prop Forwarding**: Optimize prop forwarding in higher-order components
* - **Form Field Props**: Stabilize field configurations in form libraries
*
* ### Shallow Comparison Algorithm
* 1. **Reference Check**: Return immediately if object reference unchanged
* 2. **Nullish Check**: Handle null/undefined consistently
* 3. **Key Count Comparison**: Fast path for different property counts
* 4. **Value Comparison**: Shallow comparison of all property values
* 5. **Reference Preservation**: Return previous reference if contents identical
*
* ### Performance Characteristics
* - **Best Case**: O(1) for unchanged references
* - **Typical Case**: O(n) where n = number of properties
* - **Memory**: Stores one previous reference per hook instance
*
* @example
* ```typescript
* // ❌ Problem: New object on every render
* const ExpensiveChild = React.memo(({ config }) => {
* return <div>Renders on every parent update</div>;
* });
*
* const Parent = ({ theme, user }) => {
* return (
* <ExpensiveChild
* config={{ theme, userId: user.id }} // New object every time!
* />
* );
* };
*
* // ✅ Solution: Stabilize object reference
* const Parent = ({ theme, user }) => {
* const stableConfig = useRestProperties({
* theme,
* userId: user.id
* });
*
* return <ExpensiveChild config={stableConfig} />; // Only re-renders when content changes
* };
*
* // Rest props in reusable components
* const Button = ({ variant, size, children, ...restProps }) => {
* const stableRestProps = useRestProperties(restProps);
*
* return (
* <MemoizedButton
* variant={variant}
* size={size}
* {...stableRestProps} // Stable reference prevents re-renders
* >
* {children}
* </MemoizedButton>
* );
* };
*
* // Context provider optimization
* const AuthProvider = ({ children }) => {
* const [user, setUser] = useState(null);
* const [permissions, setPermissions] = useState([]);
*
* const contextValue = useRestProperties({
* user,
* permissions,
* isAuthenticated: !!user,
* hasPermission: (perm) => permissions.includes(perm),
* login: setUser,
* logout: () => setUser(null)
* });
*
* // Consumers only re-render when auth state actually changes
* return (
* <AuthContext.Provider value={contextValue}>
* {children}
* </AuthContext.Provider>
* );
* };
*
* // Form field configuration
* const FormField = ({ name, label, validation, ...fieldProps }) => {
* const stableFieldConfig = useRestProperties({
* name,
* required: validation?.required ?? false,
* pattern: validation?.pattern,
* ...fieldProps
* });
*
* return <MemoizedInput config={stableFieldConfig} label={label} />;
* };
*
* // Table/List component props
* const DataTable = ({ data, sortBy, filterBy, pageSize }) => {
* const processedData = useMemo(() =>
* applyFiltersAndSort(data, filterBy, sortBy), [data, filterBy, sortBy]
* );
*
* const tableConfig = useRestProperties({
* items: processedData,
* totalCount: data.length,
* isEmpty: processedData.length === 0,
* pageSize,
* sortBy,
* filterBy
* });
*
* return <VirtualizedTable config={tableConfig} />;
* };
*
* // HOC with stable prop forwarding
* const withErrorBoundary = (Component) => {
* return React.memo((props) => {
* const stableProps = useRestProperties(props);
* const [hasError, setHasError] = useState(false);
*
* if (hasError) {
* return <ErrorFallback onRetry={() => setHasError(false)} />;
* }
*
* return (
* <ErrorBoundary onError={() => setHasError(true)}>
* <Component {...stableProps} />
* </ErrorBoundary>
* );
* });
* };
* ```
*
* @typeParam T - The type of the properties object (must extend Dictionary)
* @param props - The properties object to stabilize via shallow comparison
* @returns The same object reference if contents are unchanged, otherwise the new object
*/
export declare const useRestProperties: <T extends Dictionary>(props: T) => T;