Skip to content

Commit f468e42

Browse files
authored
Migrate DateTimePicker to a function component (#2050)
* Migrate DateTimePicker to a function component * Change props.value to propsValue * Use useDidUpdate hook to update DateTimePicker value
1 parent c30ea68 commit f468e42

File tree

1 file changed

+137
-135
lines changed

1 file changed

+137
-135
lines changed

src/components/dateTimePicker/index.tsx

Lines changed: 137 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import moment from 'moment';
2-
import React, {Component} from 'react';
2+
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
33
import {StyleProp, StyleSheet, ViewStyle} from 'react-native';
44
import {DateTimePickerPackage as RNDateTimePicker} from '../../optionalDependencies';
5+
import {useDidUpdate} from 'hooks';
56
import {Colors} from '../../style';
67
import Assets from '../../assets';
78
import {Constants, asBaseComponent, BaseComponentInjectedProps} from '../../commons/new';
@@ -108,79 +109,67 @@ export interface DateTimePickerProps {
108109
testID?: string;
109110
}
110111

111-
interface DateTimePickerState {
112-
prevValue?: Date;
113-
value?: Date;
114-
}
115-
116112
type DateTimePickerPropsInternal = DateTimePickerProps & BaseComponentInjectedProps;
117113

118-
class DateTimePicker extends Component<DateTimePickerPropsInternal, DateTimePickerState> {
119-
static displayName = 'DateTimePicker';
120-
121-
static defaultProps = {
122-
...TextField.defaultProps,
123-
mode: MODES.DATE
124-
};
125-
126-
chosenDate?: Date;
127-
expandable = React.createRef<ExpandableOverlayMethods>();
114+
function DateTimePicker(props: DateTimePickerPropsInternal) {
115+
const {
116+
value: propsValue,
117+
renderInput,
118+
editable,
119+
mode = MODES.DATE,
120+
dateFormat,
121+
timeFormat,
122+
dateFormatter,
123+
timeFormatter,
124+
minimumDate,
125+
maximumDate,
126+
locale,
127+
is24Hour,
128+
minuteInterval,
129+
timeZoneOffsetInMinutes,
130+
themeVariant,
131+
onChange,
132+
dialogProps,
133+
headerStyle,
134+
// @ts-expect-error
135+
useCustomTheme,
136+
testID,
137+
...others
138+
} = props;
128139

129-
constructor(props: DateTimePickerPropsInternal) {
130-
super(props);
131-
this.chosenDate = props.value;
140+
const [value, setValue] = useState(propsValue);
141+
const chosenDate = useRef(propsValue);
142+
const expandable = useRef<ExpandableOverlayMethods>();
132143

144+
useEffect(() => {
133145
if (!RNDateTimePicker) {
134146
console.error(`RNUILib DateTimePicker component requires installing "@react-native-community/datetimepicker" dependency`);
135147
}
136-
}
137-
138-
state = {
139-
prevValue: this.props.value,
140-
value: this.props.value
141-
};
148+
}, []);
142149

143-
static getDerivedStateFromProps(nextProps: DateTimePickerProps, prevState: DateTimePickerState) {
144-
if (nextProps.value !== prevState.prevValue) {
145-
return {
146-
prevValue: prevState.value,
147-
value: nextProps.value
148-
};
149-
}
150-
return null;
151-
}
152-
153-
handleChange = (event: any = {}, date: Date) => {
154-
// NOTE: will be called on Android even when there was no actual change
155-
if (event.type !== 'dismissed' && date !== undefined) {
156-
this.chosenDate = date;
157-
158-
if (Constants.isAndroid) {
159-
this.onDonePressed();
160-
}
161-
} else if (event.type === 'dismissed' && Constants.isAndroid) {
162-
this.toggleExpandableOverlay();
163-
}
164-
};
165-
166-
toggleExpandableOverlay = () => {
167-
this.expandable.current?.toggleExpandable?.();
168-
};
150+
useDidUpdate(() => {
151+
setValue(propsValue);
152+
}, [propsValue]);
169153

170-
onDonePressed = () => {
171-
this.toggleExpandableOverlay();
172-
if (Constants.isIOS && !this.chosenDate) {
173-
// since handleChange() is not called on iOS when there is no actual change
174-
this.chosenDate = new Date();
175-
}
176-
177-
this.props.onChange?.(this.chosenDate!);
178-
this.setState({value: this.chosenDate});
179-
};
154+
const _dialogProps = useMemo(() => {
155+
return {
156+
width: '100%',
157+
height: null,
158+
bottom: true,
159+
centerH: true,
160+
containerStyle: styles.dialog,
161+
testID: `${testID}.dialog`,
162+
supportedOrientations: [
163+
'portrait',
164+
'landscape',
165+
'landscape-left',
166+
'landscape-right'
167+
] as DialogProps['supportedOrientations'],
168+
...dialogProps
169+
};
170+
}, [dialogProps, testID]);
180171

181-
getStringValue = () => {
182-
const {value} = this.state;
183-
const {mode, dateFormat, timeFormat, dateFormatter, timeFormatter} = this.props;
172+
const getStringValue = () => {
184173
if (value) {
185174
switch (mode) {
186175
case MODES.DATE:
@@ -199,72 +188,59 @@ class DateTimePicker extends Component<DateTimePickerPropsInternal, DateTimePick
199188
}
200189
};
201190

202-
getDialogProps = () => {
203-
const {testID, dialogProps} = this.props;
204-
return {
205-
width: '100%',
206-
height: null,
207-
bottom: true,
208-
centerH: true,
209-
// onDismiss: this.toggleExpandableOverlay,
210-
containerStyle: styles.dialog,
211-
testID: `${testID}.dialog`,
212-
supportedOrientations: [
213-
'portrait',
214-
'landscape',
215-
'landscape-left',
216-
'landscape-right'
217-
] as DialogProps['supportedOrientations'],
218-
...dialogProps
219-
};
220-
};
191+
const toggleExpandableOverlay = useCallback(() => {
192+
expandable.current?.toggleExpandable?.();
193+
}, []);
221194

222-
renderIOSExpandableOverlay = () => {
223-
return (
224-
<>
225-
{this.renderHeader()}
226-
{this.renderDateTimePicker()}
227-
</>
228-
);
229-
};
195+
const onDonePressed = useCallback(() => {
196+
toggleExpandableOverlay();
197+
if (Constants.isIOS && !chosenDate.current) {
198+
// since handleChange() is not called on iOS when there is no actual change
199+
chosenDate.current = new Date();
200+
}
230201

231-
renderHeader() {
232-
// @ts-expect-error
233-
const {headerStyle, useCustomTheme} = this.props;
202+
onChange?.(chosenDate.current!);
203+
setValue(chosenDate.current);
204+
}, [toggleExpandableOverlay, onChange]);
234205

206+
const handleChange = useCallback((event: any = {}, date: Date) => {
207+
// NOTE: will be called on Android even when there was no actual change
208+
if (event.type !== 'dismissed' && date !== undefined) {
209+
chosenDate.current = date;
210+
211+
if (Constants.isAndroid) {
212+
onDonePressed();
213+
}
214+
} else if (event.type === 'dismissed' && Constants.isAndroid) {
215+
toggleExpandableOverlay();
216+
}
217+
},
218+
[onDonePressed, toggleExpandableOverlay]);
219+
220+
const renderHeader = () => {
235221
return (
236222
<View row spread bg-$backgroundDefault paddingH-20 style={[styles.header, headerStyle]}>
237223
<Button
238224
link
239225
iconSource={Assets.icons.x}
240226
iconStyle={{tintColor: Colors.$iconDefault}}
241-
onPress={this.toggleExpandableOverlay}
227+
onPress={toggleExpandableOverlay}
242228
/>
243-
<Button link iconSource={Assets.icons.check} useCustomTheme={useCustomTheme} onPress={this.onDonePressed}/>
229+
<Button link iconSource={Assets.icons.check} useCustomTheme={useCustomTheme} onPress={onDonePressed}/>
244230
</View>
245231
);
246-
}
247-
248-
renderAndroidDateTimePicker = ({visible}: RenderCustomOverlayProps) => {
249-
if (visible) {
250-
return this.renderDateTimePicker();
251-
}
252232
};
253233

254-
renderDateTimePicker() {
234+
const renderDateTimePicker = useCallback(() => {
255235
if (!RNDateTimePicker) {
256236
return null;
257237
}
258238

259-
const {value} = this.state;
260-
const {mode, minimumDate, maximumDate, locale, is24Hour, minuteInterval, timeZoneOffsetInMinutes, themeVariant} =
261-
this.props;
262-
263239
return (
264240
<RNDateTimePicker
265241
mode={mode}
266242
value={value || new Date()}
267-
onChange={this.handleChange}
243+
onChange={handleChange}
268244
minimumDate={minimumDate}
269245
maximumDate={maximumDate}
270246
locale={locale}
@@ -275,40 +251,66 @@ class DateTimePicker extends Component<DateTimePickerPropsInternal, DateTimePick
275251
themeVariant={themeVariant}
276252
/>
277253
);
278-
}
279-
280-
render() {
281-
// @ts-expect-error
282-
const textInputProps = TextField.extractOwnProps(this.props);
283-
const {renderInput, editable} = this.props;
254+
}, [
255+
mode,
256+
value,
257+
handleChange,
258+
minimumDate,
259+
maximumDate,
260+
locale,
261+
is24Hour,
262+
minuteInterval,
263+
timeZoneOffsetInMinutes,
264+
themeVariant
265+
]);
284266

267+
const renderIOSExpandableOverlay = () => {
285268
return (
286269
<>
287-
<ExpandableOverlay
288-
ref={this.expandable}
289-
expandableContent={Constants.isIOS ? this.renderIOSExpandableOverlay() : undefined}
290-
useDialog
291-
dialogProps={this.getDialogProps()}
292-
disabled={editable === false}
293-
// NOTE: Android picker comes with its own overlay built-in therefor we're not using ExpandableOverlay for it
294-
renderCustomOverlay={Constants.isAndroid ? this.renderAndroidDateTimePicker : undefined}
295-
>
296-
{renderInput ? (
297-
renderInput({...this.props, value: this.getStringValue()})
298-
) : (
299-
/* @ts-expect-error */
300-
<TextField
301-
{...textInputProps}
302-
expandable={!!textInputProps.renderExpandableInput}
303-
value={this.getStringValue()}
304-
/>
305-
)}
306-
</ExpandableOverlay>
270+
{renderHeader()}
271+
{renderDateTimePicker()}
307272
</>
308273
);
309-
}
274+
};
275+
276+
const renderAndroidDateTimePicker = useCallback(({visible}: RenderCustomOverlayProps) => {
277+
if (visible) {
278+
return renderDateTimePicker();
279+
}
280+
},
281+
[renderDateTimePicker]);
282+
283+
return (
284+
<>
285+
<ExpandableOverlay
286+
// @ts-expect-error
287+
ref={expandable}
288+
expandableContent={Constants.isIOS ? renderIOSExpandableOverlay() : undefined}
289+
useDialog
290+
dialogProps={_dialogProps}
291+
disabled={editable === false}
292+
// NOTE: Android picker comes with its own overlay built-in therefor we're not using ExpandableOverlay for it
293+
renderCustomOverlay={Constants.isAndroid ? renderAndroidDateTimePicker : undefined}
294+
>
295+
{renderInput ? (
296+
renderInput({...props, value: getStringValue()})
297+
) : (
298+
/* @ts-expect-error */
299+
<TextField
300+
{...others}
301+
testID={testID}
302+
editable={editable}
303+
// @ts-expect-error should be remove after completing TextField migration
304+
expandable={!!others.renderExpandableInput}
305+
value={getStringValue()}
306+
/>
307+
)}
308+
</ExpandableOverlay>
309+
</>
310+
);
310311
}
311312

313+
DateTimePicker.displayName = 'DateTimePicker';
312314
export {DateTimePicker}; // For tests
313315
export default asBaseComponent<DateTimePickerProps>(DateTimePicker);
314316

0 commit comments

Comments
 (0)