Skip to content

Commit ca0deaa

Browse files
committed
refactor(ts): forwardRef + memo wrappers, deprecate raw exports, add tests
Addresses the PR 5 batch from the July 2026 audit. - TS-01: every wrapper is now wrapped in React.memo, and per-render allocations are hoisted (useMemo for the overlay/commonProps objects and the fallback overlay colour, useCallback for the BlurSwitch change handler). The five view wrappers (BlurView, ProgressiveBlurView, VibrancyView, LiquidGlassView, LiquidGlassContainer) forward a ref to their underlying native view; on platform-fallback paths (Android/older iOS) the ref is not attached. BlurSwitch keeps memo + a hoisted handler but no ref, since its two platforms render fundamentally different components (core Switch vs the native blur switch). - TS-02: the raw ReactNative* codegen components are marked @deprecated in the public API, with a header note that they will be removed in the next major and that the wrappers should be used instead. - TS-05: replaced the single it.todo with a real suite (11 tests) covering the getFallbackOverlayColor hex/alpha math, toColorString, the exported API surface, and render smoke tests that assert the wrappers mount and forward their defaults. Added react-test-renderer + @types/react-test-renderer as dev dependencies. Verified: tsc clean, eslint clean, all 11 jest tests pass, and the refactored wrappers render in the example app (Fast Refresh) with no redbox.
1 parent 52af0ef commit ca0deaa

10 files changed

Lines changed: 505 additions & 197 deletions

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"@release-it/conventional-changelog": "^11.0.1",
7777
"@types/jest": "^29.5.5",
7878
"@types/react": "^19.0.0",
79+
"@types/react-test-renderer": "^19.1.0",
7980
"commitlint": "^19.6.1",
8081
"del-cli": "^5.1.0",
8182
"eslint": "^9.22.0",
@@ -86,6 +87,7 @@
8687
"react": "19.0.0",
8788
"react-native": "0.79.2",
8889
"react-native-builder-bob": "^0.40.11",
90+
"react-test-renderer": "19.0.0",
8991
"release-it": "^20.2.1",
9092
"turbo": "latest",
9193
"typescript": "^5.8.3"

src/BlurSwitch.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import React from 'react';
1+
import React, { memo, useCallback } from 'react';
22
import { Platform, StyleSheet, Switch } from 'react-native';
33
import type { ViewStyle, StyleProp, ColorValue } from 'react-native';
44
import ReactNativeBlurSwitch from './ReactNativeBlurSwitchNativeComponent';
55

6-
const toColorString = (
6+
type NativeBlurSwitchProps = React.ComponentProps<typeof ReactNativeBlurSwitch>;
7+
8+
export const toColorString = (
79
color: ColorValue | undefined,
810
fallback: string
911
): string => {
@@ -102,7 +104,7 @@ export interface BlurSwitchProps {
102104
* />
103105
* ```
104106
*/
105-
export const BlurSwitch: React.FC<BlurSwitchProps> = ({
107+
const BlurSwitchComponent: React.FC<BlurSwitchProps> = ({
106108
value = false,
107109
blurAmount = 10,
108110
blurRounds = 5,
@@ -113,6 +115,15 @@ export const BlurSwitch: React.FC<BlurSwitchProps> = ({
113115
style,
114116
...props
115117
}) => {
118+
const handleNativeValueChange = useCallback<
119+
NonNullable<NativeBlurSwitchProps['onValueChange']>
120+
>(
121+
(event) => {
122+
onValueChange?.(event.nativeEvent.value);
123+
},
124+
[onValueChange]
125+
);
126+
116127
if (Platform.OS === 'ios') {
117128
return (
118129
<Switch
@@ -131,9 +142,7 @@ export const BlurSwitch: React.FC<BlurSwitchProps> = ({
131142
<ReactNativeBlurSwitch
132143
style={[styles.switch, style]}
133144
value={value}
134-
onValueChange={(event) => {
135-
onValueChange?.(event.nativeEvent.value);
136-
}}
145+
onValueChange={handleNativeValueChange}
137146
blurAmount={blurAmount}
138147
blurRounds={blurRounds}
139148
thumbColor={toColorString(thumbColor, '#FFFFFF')}
@@ -145,6 +154,10 @@ export const BlurSwitch: React.FC<BlurSwitchProps> = ({
145154
);
146155
};
147156

157+
BlurSwitchComponent.displayName = 'BlurSwitch';
158+
159+
export const BlurSwitch = memo(BlurSwitchComponent);
160+
148161
const styles = StyleSheet.create({
149162
switch: {
150163
width: 65,

src/BlurView.tsx

Lines changed: 82 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { Children } from 'react';
1+
import React, { Children, forwardRef, memo, useMemo } from 'react';
22
import { Platform, StyleSheet, View } from 'react-native';
33
import type { ViewStyle, StyleProp, ColorValue } from 'react-native';
44
import ReactNativeBlurView, {
@@ -68,6 +68,9 @@ export interface BlurViewProps {
6868
children?: React.ReactNode;
6969
}
7070

71+
/** Ref to the underlying native blur view. */
72+
export type BlurViewRef = React.ComponentRef<typeof ReactNativeBlurView>;
73+
7174
/**
7275
* A cross-platform blur view component that provides native blur effects.
7376
*
@@ -78,6 +81,8 @@ export interface BlurViewProps {
7881
* effect is positioned absolutely behind the content, ensuring interactive elements
7982
* work correctly.
8083
*
84+
* A forwarded ref resolves to the underlying native blur view.
85+
*
8186
* @example
8287
* ```tsx
8388
* <BlurView
@@ -90,61 +95,89 @@ export interface BlurViewProps {
9095
* </BlurView>
9196
* ```
9297
*/
93-
export const BlurView: React.FC<BlurViewProps> = ({
94-
blurType = 'xlight',
95-
blurAmount = 10,
96-
blurRounds = 5,
97-
reducedTransparencyFallbackColor = '#FFFFFF',
98-
overlayColor,
99-
style,
100-
children,
101-
ignoreSafeArea = true,
102-
...props
103-
}) => {
104-
const overlay = { backgroundColor: overlayColor };
105-
const commonProps: BlurViewProps = {
106-
blurType,
107-
blurAmount,
108-
blurRounds,
109-
ignoreSafeArea,
110-
reducedTransparencyFallbackColor,
111-
};
112-
113-
// If no children, render the blur view directly (for background use)
114-
if (!Children.count(children)) {
115-
return (
116-
<ReactNativeBlurView
117-
style={[style, overlay]}
118-
{...commonProps}
119-
{...props}
120-
/>
98+
const BlurViewComponent = forwardRef<BlurViewRef, BlurViewProps>(
99+
(
100+
{
101+
blurType = 'xlight',
102+
blurAmount = 10,
103+
blurRounds = 5,
104+
reducedTransparencyFallbackColor = '#FFFFFF',
105+
overlayColor,
106+
style,
107+
children,
108+
ignoreSafeArea = true,
109+
...props
110+
},
111+
ref
112+
) => {
113+
const overlay = useMemo(
114+
() => ({ backgroundColor: overlayColor }),
115+
[overlayColor]
116+
);
117+
const commonProps = useMemo<BlurViewProps>(
118+
() => ({
119+
blurType,
120+
blurAmount,
121+
blurRounds,
122+
ignoreSafeArea,
123+
reducedTransparencyFallbackColor,
124+
}),
125+
[
126+
blurType,
127+
blurAmount,
128+
blurRounds,
129+
ignoreSafeArea,
130+
reducedTransparencyFallbackColor,
131+
]
121132
);
122-
}
123133

124-
// If children exist, use the style default for Android
125-
if (Platform.OS === 'android') {
126-
return (
127-
<ReactNativeBlurView style={style} {...commonProps} {...props}>
128-
<View style={[StyleSheet.absoluteFill, overlay]} />
134+
// If no children, render the blur view directly (for background use)
135+
if (!Children.count(children)) {
136+
return (
137+
<ReactNativeBlurView
138+
ref={ref}
139+
style={[style, overlay]}
140+
{...commonProps}
141+
{...props}
142+
/>
143+
);
144+
}
145+
146+
// If children exist, use the style default for Android
147+
if (Platform.OS === 'android') {
148+
return (
149+
<ReactNativeBlurView
150+
ref={ref}
151+
style={style}
152+
{...commonProps}
153+
{...props}
154+
>
155+
<View style={[StyleSheet.absoluteFill, overlay]} />
156+
157+
{children}
158+
</ReactNativeBlurView>
159+
);
160+
}
129161

162+
// If children exist, use the absolute positioning pattern for iOS and others
163+
return (
164+
<View style={[styles.container, style, overlay]}>
165+
{/* Blur effect positioned absolutely behind content */}
166+
<ReactNativeBlurView
167+
ref={ref}
168+
style={StyleSheet.absoluteFill}
169+
{...commonProps}
170+
{...props}
171+
/>
130172
{children}
131-
</ReactNativeBlurView>
173+
</View>
132174
);
133175
}
176+
);
177+
178+
BlurViewComponent.displayName = 'BlurView';
134179

135-
// If children exist, use the absolute positioning pattern for iOS and others
136-
return (
137-
<View style={[styles.container, style, overlay]}>
138-
{/* Blur effect positioned absolutely behind content */}
139-
<ReactNativeBlurView
140-
style={StyleSheet.absoluteFill}
141-
{...commonProps}
142-
{...props}
143-
/>
144-
{children}
145-
</View>
146-
);
147-
};
180+
export const BlurView = memo(BlurViewComponent);
148181

149182
export default BlurView;
150183

src/LiquidGlassContainer.tsx

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { forwardRef, memo } from 'react';
22
import { Platform, View, type ViewProps } from 'react-native';
33
import ReactNativeLiquidGlassContainer from './ReactNativeLiquidGlassContainerNativeComponent';
44

@@ -30,12 +30,18 @@ export interface LiquidGlassContainerProps extends ViewProps {
3030
* </LiquidGlassContainer>
3131
* ```
3232
*/
33-
export const LiquidGlassContainer: React.FC<LiquidGlassContainerProps> = ({
34-
spacing = 0,
35-
style,
36-
children,
37-
...rest
38-
}) => {
33+
/**
34+
* Ref to the underlying native glass container. On the fallback path
35+
* (non-iOS or iOS < 26, which render a plain View) the ref is not attached.
36+
*/
37+
export type LiquidGlassContainerRef = React.ComponentRef<
38+
typeof ReactNativeLiquidGlassContainer
39+
>;
40+
41+
const LiquidGlassContainerComponent = forwardRef<
42+
LiquidGlassContainerRef,
43+
LiquidGlassContainerProps
44+
>(({ spacing = 0, style, children, ...rest }, ref) => {
3945
const isCompatibleIOS =
4046
Platform.OS === 'ios' && parseInt(Platform.Version as string, 10) >= 26;
4147

@@ -49,10 +55,19 @@ export const LiquidGlassContainer: React.FC<LiquidGlassContainerProps> = ({
4955
}
5056

5157
return (
52-
<ReactNativeLiquidGlassContainer spacing={spacing} style={style} {...rest}>
58+
<ReactNativeLiquidGlassContainer
59+
ref={ref}
60+
spacing={spacing}
61+
style={style}
62+
{...rest}
63+
>
5364
{children}
5465
</ReactNativeLiquidGlassContainer>
5566
);
56-
};
67+
});
68+
69+
LiquidGlassContainerComponent.displayName = 'LiquidGlassContainer';
70+
71+
export const LiquidGlassContainer = memo(LiquidGlassContainerComponent);
5772

5873
export default LiquidGlassContainer;

0 commit comments

Comments
 (0)