Slot Primitive
Merges its props onto its immediate child.
Installation
Install the component via your command line.
npx expo install @rn-primitives/slot
Copy/paste the following code to ~/components/primitives/slot.tsx
import * as React from 'react';import { Image as RNImage, Pressable as RNPressable, Text as RNText, View as RNView, StyleSheet, type PressableStateCallbackType, type ImageProps as RNImageProps, type ImageStyle as RNImageStyle, type PressableProps as RNPressableProps, type TextProps as RNTextProps, type ViewProps as RNViewProps, type StyleProp,} from 'react-native';
const Pressable = React.forwardRef<React.ElementRef<typeof RNPressable>, RNPressableProps>( (props, forwardedRef) => { const { children, ...pressableSlotProps } = props;
if (!React.isValidElement(children)) { console.log('Slot.Pressable - Invalid asChild element', children); return null; }
return React.cloneElement< React.ComponentPropsWithoutRef<typeof RNPressable>, React.ElementRef<typeof RNPressable> >(isTextChildren(children) ? <></> : children, { ...mergeProps(pressableSlotProps, children.props), ref: forwardedRef ? composeRefs(forwardedRef, (children as any).ref) : (children as any).ref, }); });
Pressable.displayName = 'SlotPressable';
const View = React.forwardRef<React.ElementRef<typeof RNView>, RNViewProps>( (props, forwardedRef) => { const { children, ...viewSlotProps } = props;
if (!React.isValidElement(children)) { console.log('Slot.View - Invalid asChild element', children); return null; }
return React.cloneElement< React.ComponentPropsWithoutRef<typeof RNView>, React.ElementRef<typeof RNView> >(isTextChildren(children) ? <></> : children, { ...mergeProps(viewSlotProps, children.props), ref: forwardedRef ? composeRefs(forwardedRef, (children as any).ref) : (children as any).ref, }); });
View.displayName = 'SlotView';
const Text = React.forwardRef<React.ElementRef<typeof RNText>, RNTextProps>( (props, forwardedRef) => { const { children, ...textSlotProps } = props;
if (!React.isValidElement(children)) { console.log('Slot.Text - Invalid asChild element', children); return null; }
return React.cloneElement< React.ComponentPropsWithoutRef<typeof RNText>, React.ElementRef<typeof RNText> >(isTextChildren(children) ? <></> : children, { ...mergeProps(textSlotProps, children.props), ref: forwardedRef ? composeRefs(forwardedRef, (children as any).ref) : (children as any).ref, }); });
Text.displayName = 'SlotText';
type ImageSlotProps = RNImageProps & { children?: React.ReactNode;};
const Image = React.forwardRef<React.ElementRef<typeof RNImage>, ImageSlotProps>( (props, forwardedRef) => { const { children, ...imageSlotProps } = props;
if (!React.isValidElement(children)) { console.log('Slot.Image - Invalid asChild element', children); return null; }
return React.cloneElement< React.ComponentPropsWithoutRef<typeof RNImage>, React.ElementRef<typeof RNImage> >(isTextChildren(children) ? <></> : children, { ...mergeProps(imageSlotProps, children.props), ref: forwardedRef ? composeRefs(forwardedRef, (children as any).ref) : (children as any).ref, }); });
Image.displayName = 'SlotImage';
export { Image, Pressable, Text, View };
// This project uses code from WorkOS/Radix Primitives.// The code is licensed under the MIT License.// https://github.com/radix-ui/primitives/tree/main
function composeRefs<T>(...refs: (React.Ref<T> | undefined)[]) { return (node: T) => refs.forEach((ref) => { if (typeof ref === 'function') { ref(node); } else if (ref != null) { (ref as React.MutableRefObject<T>).current = node; } });}
type AnyProps = Record<string, any>;
function mergeProps(slotProps: AnyProps, childProps: AnyProps) { // all child props should override const overrideProps = { ...childProps };
for (const propName in childProps) { const slotPropValue = slotProps[propName]; const childPropValue = childProps[propName];
const isHandler = /^on[A-Z]/.test(propName); if (isHandler) { // if the handler exists on both, we compose them if (slotPropValue && childPropValue) { overrideProps[propName] = (...args: unknown[]) => { childPropValue(...args); slotPropValue(...args); }; } // but if it exists only on the slot, we use only this one else if (slotPropValue) { overrideProps[propName] = slotPropValue; } } // if it's `style`, we merge them else if (propName === 'style') { overrideProps[propName] = combineStyles(slotPropValue, childPropValue); } else if (propName === 'className') { overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(' '); } }
return { ...slotProps, ...overrideProps };}
type PressableStyle = RNPressableProps['style'];type ImageStyle = StyleProp<RNImageStyle>;type Style = PressableStyle | ImageStyle;
function combineStyles(slotStyle?: Style, childValue?: Style) { if (typeof slotStyle === 'function' && typeof childValue === 'function') { return (state: PressableStateCallbackType) => { return StyleSheet.flatten([slotStyle(state), childValue(state)]); }; } if (typeof slotStyle === 'function') { return (state: PressableStateCallbackType) => { return childValue ? StyleSheet.flatten([slotStyle(state), childValue]) : slotStyle(state); }; } if (typeof childValue === 'function') { return (state: PressableStateCallbackType) => { return slotStyle ? StyleSheet.flatten([slotStyle, childValue(state)]) : childValue(state); }; }
return StyleSheet.flatten([slotStyle, childValue].filter(Boolean));}
export function isTextChildren( children: React.ReactNode | ((state: PressableStateCallbackType) => React.ReactNode)) { return Array.isArray(children) ? children.every((child) => typeof child === 'string') : typeof children === 'string';}
Usage
View Slot
import * as Slot from '@rn-primitives/slot';
function CustomView({ isChild = true }) { const Component = isChild ? Slot.View : View; return ( <Component className="bg-red-500"> {/* The `className` is passed down to the `View` with `key="x"` when `isChild` is `true` */} <View key="x" /> </Component> );}
Pressable Slot
import * as Slot from '@rn-primitives/slot';
function CustomPressable() { return ( <Slot.Pressable onPress={() => { console.log("Pressed")}}> {/* The `onPress` prop is passed down to the `Pressable` */} <Pressable /> </Slot.Pressable> );}
Text Slot
import * as Slot from '@rn-primitives/slot';
function CustomText() { return ( <Slot.Text className="text-blue-500"> {/* The `className` is passed down to the `View` */} <Text /> </Slot.Text> );}
Image Slot
import * as Slot from '@rn-primitives/slot';
function CustomImage() { return ( <Slot.Image source={{ uri: "https://avatars.githubusercontent.com/u/63797719?v=4" }}> {/* The `source` is passed down to the `View` */} <Image /> </Slot.Image> );}
Props
Inherits all props from its type of Slot.