Skip to content

Commit 297b3b6

Browse files
committed
chore: Convert Modal to function component
1 parent d8f7183 commit 297b3b6

2 files changed

Lines changed: 164 additions & 180 deletions

File tree

packages/react-native/Libraries/Modal/Modal.js

Lines changed: 156 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@
1010

1111
import type {HostInstance} from '../../src/private/types/HostInstance';
1212
import type {ViewProps} from '../Components/View/ViewPropTypes';
13-
import type {RootTag} from '../ReactNative/RootTag';
1413
import type {DirectEventHandler} from '../Types/CodegenTypes';
1514

1615
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
1716
import {type ColorValue} from '../StyleSheet/StyleSheet';
18-
import {type EventSubscription} from '../vendor/emitter/EventEmitter';
1917
import NativeModalManager from './NativeModalManager';
2018
import RCTModalHostView from './RCTModalHostViewNativeComponent';
2119
import VirtualizedLists from '@react-native/virtualized-lists';
2220
import * as React from 'react';
21+
import {useCallback, useContext, useEffect, useState} from 'react';
2322

2423
const ScrollView = require('../Components/ScrollView/ScrollView').default;
2524
const View = require('../Components/View/View').default;
@@ -183,30 +182,34 @@ export type ModalProps = {
183182
...ViewProps,
184183
};
185184

186-
function confirmProps(props: ModalProps) {
185+
function confirmProps({
186+
transparent,
187+
presentationStyle,
188+
navigationBarTranslucent,
189+
statusBarTranslucent,
190+
allowSwipeDismissal,
191+
onRequestClose,
192+
}: ModalProps) {
187193
if (__DEV__) {
188194
if (
189-
props.presentationStyle &&
190-
props.presentationStyle !== 'overFullScreen' &&
191-
props.transparent === true
195+
presentationStyle &&
196+
presentationStyle !== 'overFullScreen' &&
197+
transparent === true
192198
) {
193199
console.warn(
194-
`Modal with '${props.presentationStyle}' presentation style and 'transparent' value is not supported.`,
200+
`Modal with '${presentationStyle}' presentation style and 'transparent' value is not supported.`,
195201
);
196202
}
197-
if (
198-
props.navigationBarTranslucent === true &&
199-
props.statusBarTranslucent !== true
200-
) {
203+
if (navigationBarTranslucent === true && statusBarTranslucent !== true) {
201204
console.warn(
202205
'Modal with translucent navigation bar and without translucent status bar is not supported.',
203206
);
204207
}
205208

206209
if (
207210
Platform.OS === 'ios' &&
208-
props.allowSwipeDismissal === true &&
209-
!props.onRequestClose
211+
allowSwipeDismissal === true &&
212+
!onRequestClose
210213
) {
211214
console.warn(
212215
'Modal requires the onRequestClose prop when used with `allowSwipeDismissal`. This is necessary to prevent state corruption.',
@@ -215,163 +218,161 @@ function confirmProps(props: ModalProps) {
215218
}
216219
}
217220

218-
// Create a state to track whether the Modal is rendering or not.
219-
// This is the only prop that controls whether the modal is rendered or not.
220-
type ModalState = {
221-
isRendered: boolean,
222-
};
223-
224-
class Modal extends React.Component<ModalProps, ModalState> {
225-
static defaultProps: {hardwareAccelerated: boolean, visible: boolean} = {
226-
visible: true,
227-
hardwareAccelerated: false,
228-
};
221+
const onStartShouldSetResponder = () => true; // We don't want any responder events bubbling out of the modal.
229222

230-
static contextType: React.Context<RootTag> = RootTagContext;
223+
const Modal: component(
224+
ref?: React.RefSetter<ModalInstance>,
225+
...props: ModalProps
226+
) = ({
227+
backdropColor,
228+
transparent,
229+
children,
230+
presentationStyle,
231+
animationType,
232+
onRequestClose,
233+
onShow,
234+
visible = true,
235+
hardwareAccelerated = false,
236+
ref: modalRef,
237+
statusBarTranslucent,
238+
navigationBarTranslucent,
239+
supportedOrientations,
240+
onOrientationChange,
241+
allowSwipeDismissal,
242+
testID,
243+
onDismiss,
244+
}: {
245+
ref?: React.RefSetter<PublicModalInstance>,
246+
...ModalProps,
247+
}): React.Node => {
248+
const isVisible = visible === true;
231249

232-
_identifier: number;
233-
_eventSubscription: ?EventSubscription;
250+
// Create a state to track whether the Modal is rendering or not.
251+
// This is the only prop that controls whether the modal is rendered or not.
252+
const [isRendered, setIsRendered] = useState(visible === true);
253+
const [identifier] = useState(() => uniqueModalIdentifier++);
234254

235-
constructor(props: ModalProps) {
236-
super(props);
237-
if (__DEV__) {
238-
confirmProps(props);
255+
useEffect(() => {
256+
if (!ModalEventEmitter) {
257+
return;
239258
}
240-
this._identifier = uniqueModalIdentifier++;
241-
this.state = {
242-
isRendered: props.visible === true,
243-
};
244-
}
245259

246-
componentDidMount() {
247-
// 'modalDismissed' is for the old renderer in iOS only
248-
if (ModalEventEmitter) {
249-
this._eventSubscription = ModalEventEmitter.addListener(
250-
'modalDismissed',
251-
event => {
252-
this.setState({isRendered: false}, () => {
253-
if (event.modalID === this._identifier && this.props.onDismiss) {
254-
this.props.onDismiss();
255-
}
256-
});
257-
},
258-
);
259-
}
260-
}
260+
const eventSubscription = ModalEventEmitter.addListener(
261+
'modalDismissed',
262+
event => {
263+
setIsRendered(false);
264+
if (event.modalID === identifier) {
265+
onDismiss?.();
266+
}
267+
},
268+
);
269+
return () => eventSubscription.remove();
270+
}, [onDismiss, identifier]);
261271

262-
componentWillUnmount() {
263-
if (Platform.OS === 'ios') {
264-
this.setState({isRendered: false});
272+
useEffect(() => {
273+
if (isVisible) {
274+
setIsRendered(true);
265275
}
266-
if (this._eventSubscription) {
267-
this._eventSubscription.remove();
268-
}
269-
}
276+
}, [isVisible]);
270277

271-
componentDidUpdate(prevProps: ModalProps) {
272-
if (prevProps.visible === false && this.props.visible === true) {
273-
this.setState({isRendered: true});
274-
}
278+
useEffect(() => {
279+
return () => {
280+
setIsRendered(false);
281+
};
282+
}, []);
275283

284+
useEffect(() => {
276285
if (__DEV__) {
277-
confirmProps(this.props);
286+
confirmProps({
287+
transparent,
288+
presentationStyle,
289+
navigationBarTranslucent,
290+
statusBarTranslucent,
291+
allowSwipeDismissal,
292+
onRequestClose,
293+
});
278294
}
279-
}
280-
281-
// Helper function to encapsulate platform specific logic to show or not the Modal.
282-
_shouldShowModal(): boolean {
295+
}, [
296+
transparent,
297+
presentationStyle,
298+
navigationBarTranslucent,
299+
statusBarTranslucent,
300+
allowSwipeDismissal,
301+
onRequestClose,
302+
]);
303+
304+
const rootTag = useContext(RootTagContext);
305+
306+
const onDismissIos = useCallback(() => {
307+
// OnDismiss is implemented on iOS only.
283308
if (Platform.OS === 'ios') {
284-
return this.props.visible === true || this.state.isRendered === true;
285-
}
286-
287-
return this.props.visible === true;
288-
}
289-
290-
render(): React.Node {
291-
if (!this._shouldShowModal()) {
292-
return null;
293-
}
294-
295-
// Only override backgroundColor when transparent or backdropColor are
296-
// explicitly set, so that these Modal-specific props take precedence
297-
// over the generic style prop. The default backgroundColor ('white')
298-
// is defined in styles.container below.
299-
const containerStyles: {backgroundColor?: ColorValue} = {};
300-
if (this.props.transparent === true) {
301-
containerStyles.backgroundColor = 'transparent';
302-
} else if (this.props.backdropColor != null) {
303-
containerStyles.backgroundColor = this.props.backdropColor;
304-
}
305-
306-
let animationType = this.props.animationType || 'none';
307-
308-
let presentationStyle = this.props.presentationStyle;
309-
if (!presentationStyle) {
310-
presentationStyle = 'fullScreen';
311-
if (this.props.transparent === true) {
312-
presentationStyle = 'overFullScreen';
313-
}
309+
setIsRendered(false);
310+
onDismiss?.();
314311
}
312+
}, [onDismiss]);
315313

316-
const innerChildren = __DEV__ ? (
317-
<AppContainer rootTag={this.context}>{this.props.children}</AppContainer>
318-
) : (
319-
this.props.children
320-
);
321-
322-
const onDismiss = () => {
323-
// OnDismiss is implemented on iOS only.
324-
if (Platform.OS === 'ios') {
325-
this.setState({isRendered: false}, () => {
326-
if (this.props.onDismiss) {
327-
this.props.onDismiss();
328-
}
329-
});
330-
}
331-
};
314+
const shouldShowModal =
315+
Platform.OS === 'ios' ? isRendered && isVisible : isVisible;
332316

333-
return (
334-
<RCTModalHostView
335-
/* $FlowFixMe[incompatible-type] Natural Inference rollout. See
336-
* https://fburl.com/workplace/6291gfvu */
337-
animationType={animationType}
338-
presentationStyle={presentationStyle}
339-
transparent={this.props.transparent}
340-
hardwareAccelerated={this.props.hardwareAccelerated}
341-
onRequestClose={this.props.onRequestClose}
342-
onShow={this.props.onShow}
343-
onDismiss={onDismiss}
344-
ref={this.props.modalRef}
345-
visible={this.props.visible}
346-
statusBarTranslucent={this.props.statusBarTranslucent}
347-
navigationBarTranslucent={this.props.navigationBarTranslucent}
348-
identifier={this._identifier}
349-
style={styles.modal}
350-
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
351-
onStartShouldSetResponder={this._shouldSetResponder}
352-
supportedOrientations={this.props.supportedOrientations}
353-
onOrientationChange={this.props.onOrientationChange}
354-
allowSwipeDismissal={this.props.allowSwipeDismissal}
355-
testID={this.props.testID}>
356-
<VirtualizedListContextResetter>
357-
<ScrollView.Context.Provider value={null}>
358-
<View
359-
// $FlowFixMe[incompatible-type]
360-
style={[styles.container, this.props.style, containerStyles]}
361-
collapsable={false}>
362-
{innerChildren}
363-
</View>
364-
</ScrollView.Context.Provider>
365-
</VirtualizedListContextResetter>
366-
</RCTModalHostView>
367-
);
317+
if (!shouldShowModal) {
318+
return null;
368319
}
369320

370-
// We don't want any responder events bubbling out of the modal.
371-
_shouldSetResponder(): boolean {
372-
return true;
321+
const isTransparent = transparent === true;
322+
323+
// Only override backgroundColor when transparent or backdropColor are
324+
// explicitly set, so that these Modal-specific props take precedence
325+
// over the generic style prop. The default backgroundColor ('white')
326+
// is defined in styles.container below.
327+
const containerStyles = {};
328+
if (this.props.transparent === true) {
329+
containerStyles.backgroundColor = 'transparent';
330+
} else if (this.props.backdropColor != null) {
331+
containerStyles.backgroundColor = this.props.backdropColor;
373332
}
374-
}
333+
334+
return (
335+
<RCTModalHostView
336+
/* $FlowFixMe[incompatible-type] Natural Inference rollout. See
337+
* https://fburl.com/workplace/6291gfvu */
338+
animationType={animationType || 'none'}
339+
presentationStyle={
340+
presentationStyle || (isTransparent ? 'overFullScreen' : 'fullScreen')
341+
}
342+
transparent={transparent}
343+
hardwareAccelerated={hardwareAccelerated}
344+
onRequestClose={onRequestClose}
345+
onShow={onShow}
346+
onDismiss={onDismissIos}
347+
ref={modalRef}
348+
visible={visible}
349+
statusBarTranslucent={statusBarTranslucent}
350+
navigationBarTranslucent={navigationBarTranslucent}
351+
identifier={identifier}
352+
style={styles.modal}
353+
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
354+
onStartShouldSetResponder={onStartShouldSetResponder}
355+
supportedOrientations={supportedOrientations}
356+
onOrientationChange={onOrientationChange}
357+
allowSwipeDismissal={allowSwipeDismissal}
358+
testID={testID}>
359+
<VirtualizedListContextResetter>
360+
<ScrollView.Context.Provider value={null}>
361+
<View
362+
// $FlowFixMe[incompatible-type]
363+
style={[styles.container, this.props.style, containerStyles]}
364+
collapsable={false}>
365+
{__DEV__ ? (
366+
<AppContainer rootTag={rootTag}>{children}</AppContainer>
367+
) : (
368+
children
369+
)}
370+
</View>
371+
</ScrollView.Context.Provider>
372+
</VirtualizedListContextResetter>
373+
</RCTModalHostView>
374+
);
375+
};
375376

376377
const side = I18nManager.getConstants().isRTL ? 'right' : 'left';
377378
const styles = StyleSheet.create({
@@ -392,25 +393,9 @@ const styles = StyleSheet.create({
392393
},
393394
});
394395

395-
type ModalRefProps = Readonly<{
396-
ref?: React.RefSetter<ModalInstance>,
397-
}>;
398-
399-
// NOTE: This wrapper component is necessary because `Modal` is a class
400-
// component and we need to map `ref` to a differently named prop. This can be
401-
// removed when `Modal` is a functional component.
402-
function Wrapper({
403-
ref,
404-
...props
405-
}: {
406-
...ModalRefProps,
407-
...ModalProps,
408-
}): React.Node {
409-
return <Modal {...props} modalRef={ref} />;
410-
}
396+
Modal.displayName = 'Modal';
411397

412-
Wrapper.displayName = 'Modal';
413398
// $FlowExpectedError[prop-missing]
414-
Wrapper.Context = VirtualizedListContextResetter;
399+
Modal.Context = VirtualizedListContextResetter;
415400

416-
export default Wrapper;
401+
export default Modal;

0 commit comments

Comments
 (0)