From 072e40bcaacb1f63587b75c3bc417837692bbd38 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 19 Oct 2021 06:14:11 -0700 Subject: [PATCH] refactor(modal): migrate to functional component (#2557) --- src/components/Modal.tsx | 221 +++++++++++++++++---------------------- 1 file changed, 97 insertions(+), 124 deletions(-) diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 5ae2f10c6f..6ede775988 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -15,7 +15,8 @@ import { getBottomSpace, } from 'react-native-iphone-x-helper'; import Surface from './Surface'; -import { withTheme } from '../core/theming'; +import { useTheme } from '../core/theming'; +import useAnimatedValue from '../utils/useAnimatedValue'; type Props = { /** @@ -47,15 +48,6 @@ type Props = { * Use this prop to change the default wrapper style or to override safe area insets with marginTop and marginBottom. */ style?: StyleProp; - /** - * @optional - */ - theme: ReactNativePaper.Theme; -}; - -type State = { - opacity: Animated.Value; - rendered: boolean; }; const DEFAULT_DURATION = 220; @@ -101,60 +93,54 @@ const BOTTOM_INSET = getBottomSpace(); * export default MyComponent; * ``` */ -class Modal extends React.Component { - static defaultProps = { - dismissable: true, - visible: false, - overlayAccessibilityLabel: 'Close modal', - }; +export default function Modal({ + dismissable = true, + visible = false, + overlayAccessibilityLabel = 'Close modal', + onDismiss, + children, + contentContainerStyle, + style, +}: Props) { + const visibleRef = React.useRef(visible); - static getDerivedStateFromProps(nextProps: Props, prevState: State) { - if (nextProps.visible && !prevState.rendered) { - return { - rendered: true, - }; - } + React.useEffect(() => { + visibleRef.current = visible; + }); - return null; - } + const { colors, animation } = useTheme(); - state = { - opacity: new Animated.Value(this.props.visible ? 1 : 0), - rendered: this.props.visible, - }; + const opacity = useAnimatedValue(visible ? 1 : 0); - componentDidUpdate(prevProps: Props) { - if (prevProps.visible !== this.props.visible) { - if (this.props.visible) { - this.showModal(); - } else { - this.hideModal(); - } - } - } + const [rendered, setRendered] = React.useState(visible); - private subscription: NativeEventSubscription | undefined; + if (visible && !rendered) { + setRendered(true); + } - private handleBack = () => { - if (this.props.dismissable) { - this.hideModal(); + const handleBack = () => { + if (dismissable) { + hideModal(); } return true; }; - private showModal = () => { - if (this.subscription?.remove) { - this.subscription.remove(); + const subscription = React.useRef( + undefined + ); + + const showModal = () => { + if (subscription.current?.remove) { + subscription.current.remove(); } else { - BackHandler.removeEventListener('hardwareBackPress', this.handleBack); + BackHandler.removeEventListener('hardwareBackPress', handleBack); } - this.subscription = BackHandler.addEventListener( + subscription.current = BackHandler.addEventListener( 'hardwareBackPress', - this.handleBack + handleBack ); - const { opacity } = this.state; - const { scale } = this.props.theme.animation; + const { scale } = animation; Animated.timing(opacity, { toValue: 1, @@ -164,15 +150,14 @@ class Modal extends React.Component { }).start(); }; - private hideModal = () => { - if (this.subscription?.remove) { - this.subscription?.remove(); + const hideModal = () => { + if (subscription.current?.remove) { + subscription.current?.remove(); } else { - BackHandler.removeEventListener('hardwareBackPress', this.handleBack); + BackHandler.removeEventListener('hardwareBackPress', handleBack); } - const { opacity } = this.state; - const { scale } = this.props.theme.animation; + const { scale } = animation; Animated.timing(opacity, { toValue: 0, @@ -184,89 +169,77 @@ class Modal extends React.Component { return; } - if (this.props.visible && this.props.onDismiss) { - this.props.onDismiss(); + if (visible && onDismiss) { + onDismiss(); } - if (this.props.visible) { - this.showModal(); + if (visibleRef.current) { + showModal(); } else { - this.setState({ - rendered: false, - }); + setRendered(false); } }); }; - componentWillUnmount() { - if (this.subscription?.remove) { - this.subscription.remove(); - } else { - BackHandler.removeEventListener('hardwareBackPress', this.handleBack); - } - } - - render() { - const { rendered, opacity } = this.state; + const prevVisible = React.useRef(null); - if (!rendered) return null; - - const { - children, - dismissable, - style, - theme, - contentContainerStyle, - overlayAccessibilityLabel, - } = this.props; - const { colors } = theme; - return ( - { + if (prevVisible.current !== visible) { + if (visible) { + showModal(); + } else { + hideModal(); + } + } + prevVisible.current = visible; + }); + + if (!rendered) return null; + + return ( + + - - - - + + + + } > - - } - > - {children} - - - - ); - } + {children} + + + + ); } -export default withTheme(Modal); - const styles = StyleSheet.create({ backdrop: { flex: 1,