Skip to content

Commit f93cb20

Browse files
mdwyer6Michael Dwyer
andauthored
fix: make empty SlotsProvider context stable (#7485)
* make empty SlotsProvider context stable * move empty object into useMemo * lint fixes * update tests for strict mode * delete whitespace * remove useMemo for content * delete newline * test renders without reliance on strictmode details --------- Co-authored-by: Michael Dwyer <[email protected]>
1 parent 326f481 commit f93cb20

File tree

2 files changed

+104
-5
lines changed

2 files changed

+104
-5
lines changed

packages/@react-spectrum/utils/src/Slots.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ export function cssModuleToSlots(cssModule) {
3535
}
3636

3737
export function SlotProvider(props) {
38+
const emptyObj = useMemo(() => ({}), []);
3839
// eslint-disable-next-line react-hooks/exhaustive-deps
39-
let parentSlots = useContext(SlotContext) || {};
40-
let {slots = {}, children} = props;
40+
let parentSlots = useContext(SlotContext) || emptyObj;
41+
let {slots = emptyObj, children} = props;
4142

4243
// Merge props for each slot from parent context and props
4344
let value = useMemo(() =>
@@ -57,14 +58,17 @@ export function SlotProvider(props) {
5758

5859
export function ClearSlots(props) {
5960
let {children, ...otherProps} = props;
61+
62+
const emptyObj = useMemo(() => ({}), []);
63+
6064
let content = children;
6165
if (React.Children.toArray(children).length <= 1) {
6266
if (typeof children === 'function') { // need to know if the node is a string or something else that react can render that doesn't get props
6367
content = React.cloneElement(React.Children.only(children), otherProps);
6468
}
6569
}
6670
return (
67-
<SlotContext.Provider value={{}}>
71+
<SlotContext.Provider value={emptyObj}>
6872
{content}
6973
</SlotContext.Provider>
7074
);

packages/@react-spectrum/utils/test/Slots.test.js

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13+
import {ClearSlots, SlotProvider, useSlotProps} from '../';
1314
import {pointerMap, render} from '@react-spectrum/test-utils-internal';
14-
import React, {useRef} from 'react';
15-
import {SlotProvider, useSlotProps} from '../';
15+
import React, {StrictMode, useRef} from 'react';
1616
import {useId, useSlotId} from '@react-aria/utils';
1717
import {usePress} from '@react-aria/interactions';
1818
import userEvent from '@testing-library/user-event';
@@ -190,4 +190,99 @@ describe('Slots', function () {
190190
expect(getByRole('presentation')).toHaveAttribute('aria-controls', id);
191191
});
192192

193+
it('does not rerender slots consumers when the slot provider rerenders with stable values', function () {
194+
let slots = {
195+
slotname: {label: 'foo'}
196+
};
197+
let renderCount = 0;
198+
199+
const TestComponent = (props) => {
200+
useSlotProps(props, 'slotname');
201+
React.useEffect(() => {
202+
renderCount++;
203+
});
204+
205+
return <p>test component</p>;
206+
};
207+
208+
const MemoizedComponent = React.memo(function MemoizedComponent(props) {
209+
return props.children;
210+
});
211+
212+
const FullComponentTree = () => {
213+
const StableTestComponent = React.useMemo(() => <TestComponent prop1="value1" />, []);
214+
215+
return (
216+
<StrictMode>
217+
<SlotProvider>
218+
<MemoizedComponent>
219+
{StableTestComponent}
220+
</MemoizedComponent>
221+
</SlotProvider>
222+
</StrictMode>
223+
);
224+
};
225+
226+
const {rerender} = render(
227+
<FullComponentTree slots={slots} />
228+
);
229+
230+
let renderCountBeforeRerender = renderCount;
231+
232+
// Trigger a rerender with the same stable props
233+
rerender(
234+
<FullComponentTree slots={slots} />
235+
);
236+
237+
expect(renderCount).toEqual(renderCountBeforeRerender);
238+
});
239+
240+
it('does not rerender slots consumers when <ClearSlot /> wrapper is placed between SlotProvider and Consumer', function () {
241+
let slots = {
242+
slotname: {label: 'foo'}
243+
};
244+
let renderCount = 0;
245+
246+
const TestComponent = (props) => {
247+
useSlotProps(props, 'slotname');
248+
React.useEffect(() => {
249+
renderCount++;
250+
});
251+
252+
return <p>test component</p>;
253+
};
254+
255+
const MemoizedComponent = React.memo(function MemoizedComponent(props) {
256+
return props.children;
257+
});
258+
259+
const FullComponentTree = () => {
260+
const StableTestComponent = React.useMemo(() => <TestComponent prop1="value1" />, []);
261+
262+
return (
263+
<StrictMode>
264+
<SlotProvider>
265+
<MemoizedComponent>
266+
<ClearSlots>
267+
{StableTestComponent}
268+
</ClearSlots>
269+
</MemoizedComponent>
270+
</SlotProvider>
271+
</StrictMode>
272+
);
273+
};
274+
275+
const {rerender} = render(
276+
<FullComponentTree slots={slots} />
277+
);
278+
279+
let renderCountBeforeRerender = renderCount;
280+
281+
// Trigger a rerender with the same stable props
282+
rerender(
283+
<FullComponentTree slots={slots} />
284+
);
285+
286+
expect(renderCount).toEqual(renderCountBeforeRerender);
287+
});
193288
});

0 commit comments

Comments
 (0)