|
| 1 | +import { Meta, Canvas, Story, Controls } from '@storybook/addon-docs/blocks'; |
| 2 | +import * as RenderCacheStories from './RenderCache.stories'; |
| 3 | + |
| 4 | +<Meta of={RenderCacheStories} /> |
| 5 | + |
| 6 | +# RenderCache |
| 7 | + |
| 8 | +RenderCache is a performance optimization component that caches rendered React elements and only re-renders specific items when needed. It's useful for lists where most items remain unchanged but you need fine-grained control over which items to re-render. |
| 9 | + |
| 10 | +## When to Use |
| 11 | + |
| 12 | +- When rendering lists where only specific items need to re-render based on state changes |
| 13 | +- When child components are expensive to render and don't need to update on every parent re-render |
| 14 | +- When you want more control than React.memo provides, with explicit per-item cache invalidation |
| 15 | +- When building complex UI where selective re-rendering improves performance significantly |
| 16 | + |
| 17 | +## Component |
| 18 | + |
| 19 | +<Story of={RenderCacheStories.Default} /> |
| 20 | + |
| 21 | +--- |
| 22 | + |
| 23 | +### Properties |
| 24 | + |
| 25 | +<Controls of={RenderCacheStories.Default} /> |
| 26 | + |
| 27 | +### Property Details |
| 28 | + |
| 29 | +- **items**: Array of items to render |
| 30 | +- **renderKeys**: Array of keys that should trigger re-render. Only items with keys in this array will be re-rendered |
| 31 | +- **getKey**: Function that extracts a unique key from each item |
| 32 | +- **children**: Render function that takes an item and returns a React element |
| 33 | + |
| 34 | +### Base Properties |
| 35 | + |
| 36 | +This is a headless component and does not support base properties. |
| 37 | + |
| 38 | +## How It Works |
| 39 | + |
| 40 | +The component maintains an internal cache of rendered elements. When rendering: |
| 41 | + |
| 42 | +1. For each item, it checks if the item's key is in renderKeys |
| 43 | +2. If the key is in renderKeys or the item hasn't been rendered before, it calls the children function to render/re-render |
| 44 | +3. Otherwise, it returns the cached element from the previous render |
| 45 | +4. It automatically cleans up cache entries for items that are no longer in the list |
| 46 | + |
| 47 | +## Examples |
| 48 | + |
| 49 | +### Basic Usage |
| 50 | + |
| 51 | +```jsx |
| 52 | +import { RenderCache } from '@cube-dev/ui-kit'; |
| 53 | + |
| 54 | +const items = [ |
| 55 | + { id: 1, name: 'Item 1' }, |
| 56 | + { id: 2, name: 'Item 2' }, |
| 57 | + { id: 3, name: 'Item 3' }, |
| 58 | +]; |
| 59 | + |
| 60 | +const [selectedId, setSelectedId] = useState(1); |
| 61 | + |
| 62 | +<RenderCache |
| 63 | + items={items} |
| 64 | + renderKeys={[selectedId]} |
| 65 | + getKey={(item) => item.id} |
| 66 | +> |
| 67 | + {(item) => <ExpensiveItem item={item} isSelected={item.id === selectedId} />} |
| 68 | +</RenderCache> |
| 69 | +``` |
| 70 | + |
| 71 | +### With List of Expensive Components |
| 72 | + |
| 73 | +```jsx |
| 74 | +import { RenderCache } from '@cube-dev/ui-kit'; |
| 75 | + |
| 76 | +function ExpensiveListItem({ item, isActive }) { |
| 77 | + // Complex rendering logic |
| 78 | + return <div className={isActive ? 'active' : ''}>{item.name}</div>; |
| 79 | +} |
| 80 | + |
| 81 | +const [activeId, setActiveId] = useState(1); |
| 82 | + |
| 83 | +<RenderCache |
| 84 | + items={data} |
| 85 | + renderKeys={[activeId]} |
| 86 | + getKey={(item) => item.id} |
| 87 | +> |
| 88 | + {(item) => ( |
| 89 | + <ExpensiveListItem |
| 90 | + item={item} |
| 91 | + isActive={item.id === activeId} |
| 92 | + /> |
| 93 | + )} |
| 94 | +</RenderCache> |
| 95 | +``` |
| 96 | + |
| 97 | +## Performance Considerations |
| 98 | + |
| 99 | +- **Cache management**: The component uses useRef to maintain the cache across renders, avoiding unnecessary re-allocations |
| 100 | +- **Automatic cleanup**: Removes cached entries for items no longer in the list to prevent memory leaks |
| 101 | +- **Selective invalidation**: Only items with keys in renderKeys are re-rendered, others use cached elements |
| 102 | +- **Best for expensive renders**: Most effective when child components have significant render cost |
| 103 | + |
| 104 | +## Best Practices |
| 105 | + |
| 106 | +1. **Use stable keys**: Ensure getKey returns consistent keys for the same items across renders |
| 107 | + ```jsx |
| 108 | + // Good: Using stable IDs |
| 109 | + <RenderCache |
| 110 | + items={items} |
| 111 | + getKey={(item) => item.id} |
| 112 | + renderKeys={[selectedId]} |
| 113 | + > |
| 114 | + {(item) => <Item {...item} />} |
| 115 | + </RenderCache> |
| 116 | + |
| 117 | + // Bad: Using indices (unstable when list changes) |
| 118 | + <RenderCache |
| 119 | + items={items} |
| 120 | + getKey={(item, index) => index} |
| 121 | + renderKeys={[selectedIndex]} |
| 122 | + > |
| 123 | + {(item) => <Item {...item} />} |
| 124 | + </RenderCache> |
| 125 | + ``` |
| 126 | + |
| 127 | +2. **Minimize renderKeys**: Only include keys that truly need re-rendering to maximize cache benefits |
| 128 | + ```jsx |
| 129 | + // Good: Only re-render the active item |
| 130 | + <RenderCache |
| 131 | + items={items} |
| 132 | + renderKeys={[activeItemId]} |
| 133 | + getKey={(item) => item.id} |
| 134 | + > |
| 135 | + {(item) => <Item {...item} />} |
| 136 | + </RenderCache> |
| 137 | + |
| 138 | + // Bad: Re-rendering all items defeats the purpose |
| 139 | + <RenderCache |
| 140 | + items={items} |
| 141 | + renderKeys={items.map(item => item.id)} |
| 142 | + getKey={(item) => item.id} |
| 143 | + > |
| 144 | + {(item) => <Item {...item} />} |
| 145 | + </RenderCache> |
| 146 | + ``` |
| 147 | + |
| 148 | +3. **Consider alternatives**: For simple cases, React.memo might be sufficient and simpler |
| 149 | + |
| 150 | +4. **Profile first**: Use React DevTools Profiler to confirm you have a performance issue before adding this optimization |
| 151 | + |
| 152 | +5. **Avoid inline functions**: Use stable render functions to prevent unnecessary cache invalidation |
| 153 | + ```jsx |
| 154 | + // Good: Stable render function |
| 155 | + const renderItem = useCallback((item) => ( |
| 156 | + <ExpensiveItem item={item} /> |
| 157 | + ), []); |
| 158 | + |
| 159 | + <RenderCache |
| 160 | + items={items} |
| 161 | + renderKeys={[selectedId]} |
| 162 | + getKey={(item) => item.id} |
| 163 | + > |
| 164 | + {renderItem} |
| 165 | + </RenderCache> |
| 166 | + ``` |
| 167 | + |
| 168 | +## When NOT to Use |
| 169 | + |
| 170 | +- **Simple lists**: For simple lists without performance issues, regular rendering is simpler |
| 171 | +- **All items update frequently**: If all items need to re-render on every change, the cache overhead isn't worth it |
| 172 | +- **Small lists**: Lists with < 10 items typically don't benefit from caching |
| 173 | +- **Simple components**: If child components render quickly, the optimization overhead may outweigh benefits |
| 174 | + |
| 175 | +## Related Components |
| 176 | + |
| 177 | +- **React.memo** - For simple component memoization |
| 178 | +- **useMemo** - For memoizing computed values |
| 179 | +- **useCallback** - For memoizing callback functions |
| 180 | +- **DisplayTransition** - For animating component mount/unmount |
| 181 | + |
0 commit comments