@@ -55,6 +55,11 @@ export interface ControlledTreeProps<T> {
5555 * Notable differences from older tree implementations (like Blueprint's Tree component):
5656 * - Node value string represent not just the id of the current node, but the full path (delimited by
5757 * slashes) of all the node ids from the root node to the current node.
58+ *
59+ * Performance notes:
60+ * - Event handlers are memoized to prevent re-renders on hover
61+ * - Component uses custom memo comparison to avoid re-renders when array contents haven't changed
62+ * - If still experiencing performance issues, check parent components for frequently changing props
5863 */
5964function ControlledTree < T extends object > ( {
6065 nodes,
@@ -115,8 +120,61 @@ function ControlledTree<T extends object>({
115120 }
116121 } , [ checkedState , nodes , onSelect , selectionMode ] ) ;
117122
123+ // Memoized event handlers to prevent re-renders
124+ const handleCheckboxClick = useCallback (
125+ ( nodeValue : string , checked : boolean ) => {
126+ if ( checked ) {
127+ tree . uncheckNode ( nodeValue ) ;
128+ } else {
129+ tree . checkNode ( nodeValue ) ;
130+ }
131+ } ,
132+ [ tree ] ,
133+ ) ;
134+
135+ const handleExpandClick = useCallback (
136+ ( nodeValue : string , expanded : boolean ) => {
137+ if ( expanded ) {
138+ tree . collapse ( nodeValue ) ;
139+ } else {
140+ tree . expand ( nodeValue ) ;
141+ }
142+ } ,
143+ [ tree ] ,
144+ ) ;
145+
146+ const handleLabelClick = useCallback (
147+ ( nodeValue : string , selected : boolean ) => {
148+ if ( selectionMode !== "single" ) {
149+ return ;
150+ }
151+
152+ if ( selected ) {
153+ tree . deselect ( nodeValue ) ;
154+ } else {
155+ tree . select ( nodeValue ) ;
156+ const selectedNode = findNodeById ( nodes , mantineNodeValueToId ( nodeValue ) ) ;
157+ if ( selectedNode ) {
158+ onSelect ?.( [ selectedNode ] ) ;
159+ }
160+ }
161+ } ,
162+ [ selectionMode , tree , nodes , onSelect ] ,
163+ ) ;
164+
165+ const handleSelectAllClick = useCallback (
166+ ( allChecked : boolean ) => {
167+ if ( allChecked ) {
168+ tree . uncheckAllNodes ( ) ;
169+ } else {
170+ tree . checkAllNodes ( ) ;
171+ }
172+ } ,
173+ [ tree ] ,
174+ ) ;
175+
118176 const renderTreeNode = useCallback (
119- ( { node, expanded, selected, hasChildren, elementProps, tree } : RenderTreeNodePayload ) => {
177+ ( { node, expanded, selected, hasChildren, elementProps } : RenderTreeNodePayload ) => {
120178 const checked = tree . isNodeChecked ( node . value ) ;
121179 const indeterminate = tree . isNodeIndeterminate ( node . value ) ;
122180
@@ -135,11 +193,7 @@ function ControlledTree<T extends object>({
135193 checked = { checked }
136194 indeterminate = { indeterminate }
137195 onClick = { ( ) => {
138- if ( checked ) {
139- tree . uncheckNode ( node . value ) ;
140- } else {
141- tree . checkNode ( node . value ) ;
142- }
196+ handleCheckboxClick ( node . value , checked ) ;
143197 } }
144198 />
145199 ) }
@@ -151,11 +205,7 @@ function ControlledTree<T extends object>({
151205 color = "gray"
152206 variant = "subtle"
153207 onClick = { ( ) => {
154- if ( expanded ) {
155- tree . collapse ( node . value ) ;
156- } else {
157- tree . expand ( node . value ) ;
158- }
208+ handleExpandClick ( node . value , expanded ) ;
159209 } }
160210 >
161211 { expanded ? < IoChevronDown /> : < IoChevronForward /> }
@@ -165,27 +215,15 @@ function ControlledTree<T extends object>({
165215 < div
166216 className = { styles . labelContainer }
167217 onClick = { ( ) => {
168- if ( selectionMode !== "single" ) {
169- return ;
170- }
171-
172- if ( selected ) {
173- tree . deselect ( node . value ) ;
174- } else {
175- tree . select ( node . value ) ;
176- const selectedNode = findNodeById ( nodes , mantineNodeValueToId ( node . value ) ) ;
177- if ( selectedNode ) {
178- onSelect ?.( [ selectedNode ] ) ;
179- }
180- }
218+ handleLabelClick ( node . value , selected ) ;
181219 } }
182220 >
183221 < span > { node . label } </ span >
184222 </ div >
185223 </ div >
186224 ) ;
187225 } ,
188- [ nodes , onSelect , selectionMode ] ,
226+ [ selectionMode , tree , handleCheckboxClick , handleExpandClick , handleLabelClick ] ,
189227 ) ;
190228
191229 // Update tree state controlled selection changes
@@ -209,11 +247,7 @@ function ControlledTree<T extends object>({
209247 checked = { allNodesChecked }
210248 indeterminate = { ! allNodesChecked && someNodesChecked }
211249 onClick = { ( ) => {
212- if ( allNodesChecked ) {
213- tree . uncheckAllNodes ( ) ;
214- } else {
215- tree . checkAllNodes ( ) ;
216- }
250+ handleSelectAllClick ( allNodesChecked ) ;
217251 } }
218252 />
219253 < Text pl = { 5 } fw = { 600 } >
@@ -232,7 +266,33 @@ function ControlledTree<T extends object>({
232266 ) ;
233267}
234268
235- export default memo ( ControlledTree ) as typeof ControlledTree ;
269+ export default memo ( ControlledTree , ( prevProps , nextProps ) => {
270+ // Custom comparison to prevent re-renders when array contents haven't changed
271+ if ( prevProps . selectionMode !== nextProps . selectionMode ) {
272+ return false ;
273+ }
274+
275+ if ( prevProps . nodes !== nextProps . nodes ) {
276+ return false ;
277+ }
278+
279+ if ( prevProps . onSelect !== nextProps . onSelect ) {
280+ return false ;
281+ }
282+
283+ // Deep comparison for selectedNodeIds array
284+ if ( prevProps . selectedNodeIds . length !== nextProps . selectedNodeIds . length ) {
285+ return false ;
286+ }
287+
288+ for ( let i = 0 ; i < prevProps . selectedNodeIds . length ; i ++ ) {
289+ if ( prevProps . selectedNodeIds [ i ] !== nextProps . selectedNodeIds [ i ] ) {
290+ return false ;
291+ }
292+ }
293+
294+ return true ;
295+ } ) as typeof ControlledTree ;
236296
237297// UTILITIES
238298// -------------------------------------------------------------------------------------------------
0 commit comments