Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions packages/@react-spectrum/utils/src/Slots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ export function cssModuleToSlots(cssModule) {
}

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

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

export function ClearSlots(props) {
let {children, ...otherProps} = props;

const emptyObj = useMemo(() => ({}), []);

let content = children;
if (React.Children.toArray(children).length <= 1) {
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
content = React.cloneElement(React.Children.only(children), otherProps);
}
}
return (
<SlotContext.Provider value={{}}>
<SlotContext.Provider value={emptyObj}>
{content}
</SlotContext.Provider>
);
Expand Down
99 changes: 97 additions & 2 deletions packages/@react-spectrum/utils/test/Slots.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
* governing permissions and limitations under the License.
*/

import {ClearSlots, SlotProvider, useSlotProps} from '../';
import {pointerMap, render} from '@react-spectrum/test-utils-internal';
import React, {useRef} from 'react';
import {SlotProvider, useSlotProps} from '../';
import React, {StrictMode, useRef} from 'react';
import {useId, useSlotId} from '@react-aria/utils';
import {usePress} from '@react-aria/interactions';
import userEvent from '@testing-library/user-event';
Expand Down Expand Up @@ -190,4 +190,99 @@ describe('Slots', function () {
expect(getByRole('presentation')).toHaveAttribute('aria-controls', id);
});

it('does not rerender slots consumers when the slot provider rerenders with stable values', function () {
let slots = {
slotname: {label: 'foo'}
};
let renderCount = 0;

const TestComponent = (props) => {
useSlotProps(props, 'slotname');
React.useEffect(() => {
renderCount++;
});

return <p>test component</p>;
};

const MemoizedComponent = React.memo(function MemoizedComponent(props) {
return props.children;
});

const FullComponentTree = () => {
const StableTestComponent = React.useMemo(() => <TestComponent prop1="value1" />, []);

return (
<StrictMode>
<SlotProvider>
<MemoizedComponent>
{StableTestComponent}
</MemoizedComponent>
</SlotProvider>
</StrictMode>
);
};

const {rerender} = render(
<FullComponentTree slots={slots} />
);

let renderCountBeforeRerender = renderCount;

// Trigger a rerender with the same stable props
rerender(
<FullComponentTree slots={slots} />
);

expect(renderCount).toEqual(renderCountBeforeRerender);
});

it('does not rerender slots consumers when <ClearSlot /> wrapper is placed between SlotProvider and Consumer', function () {
let slots = {
slotname: {label: 'foo'}
};
let renderCount = 0;

const TestComponent = (props) => {
useSlotProps(props, 'slotname');
React.useEffect(() => {
renderCount++;
});

return <p>test component</p>;
};

const MemoizedComponent = React.memo(function MemoizedComponent(props) {
return props.children;
});

const FullComponentTree = () => {
const StableTestComponent = React.useMemo(() => <TestComponent prop1="value1" />, []);

return (
<StrictMode>
<SlotProvider>
<MemoizedComponent>
<ClearSlots>
{StableTestComponent}
</ClearSlots>
</MemoizedComponent>
</SlotProvider>
</StrictMode>
);
};

const {rerender} = render(
<FullComponentTree slots={slots} />
);

let renderCountBeforeRerender = renderCount;

// Trigger a rerender with the same stable props
rerender(
<FullComponentTree slots={slots} />
);

expect(renderCount).toEqual(renderCountBeforeRerender);
});
});