Skip to content

Picker Refactor #1 #1000

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 8, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 115 additions & 130 deletions src/components/picker/PickerItem.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import React, {useCallback, useEffect, useMemo} from 'react';
import {StyleSheet} from 'react-native';
import {Colors, Typography} from '../../style';
import {BaseComponent} from '../../commons';
import * as Modifiers from '../../commons/modifiers';
import Assets from '../../assets';
import View from '../view';
import TouchableOpacity from '../touchableOpacity';
Expand All @@ -16,153 +16,138 @@ import Text from '../text';
* @extendslink: docs/TouchableOpacity
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/PickerScreen.js
*/
class PickerItem extends BaseComponent {
static displayName = 'Picker.Item';

static propTypes = {
/**
* Item's value
*/
value: PropTypes.oneOfType([PropTypes.object, PropTypes.string, PropTypes.number]),
/**
* Item's label
*/
label: PropTypes.string,
/**
* Custom function for the item label (e.g (value) => customLabel)
*/
getItemLabel: PropTypes.func,
/**
* DEPRECATE: Function to return the value out of the item value prop when value is custom shaped.
*/
getItemValue: PropTypes.func,
/**
* Is the item selected
*/
isSelected: PropTypes.bool,
/**
* Pass to change the selected icon
*/
selectedIcon: PropTypes.oneOfType([PropTypes.object, PropTypes.number]),
/**
* Pass to change the selected icon color
*/
selectedIconColor: PropTypes.string,
/**
* Is the item disabled
*/
disabled: PropTypes.bool,
/**
* Render custom item
*/
renderItem: PropTypes.elementType,
/**
* Callback for onPress action
*/
onPress: PropTypes.func,
/**
* Callback for onLayout event
*/
onSelectedLayout: PropTypes.func
};


constructor(props) {
super(props);

if (_.isPlainObject(props.value)) {
const PickerItem = props => {
const {
renderItem,
value,
label,
onPress,
disabled,
isSelected,
selectedIcon = Assets.icons.check,
selectedIconColor = Colors.primary,
testID
} = props;

useEffect(() => {
if (_.isPlainObject(value)) {
console.warn('UILib Picker.Item will stop supporting passing object as value & label (e.g {value, label}) in the next major version. Please pass separate label and value props');
}
}

}, [value]);

generateStyles() {
this.styles = createStyles(this.props);
}
// TODO: deprecate the check for object
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO hints for isObject check, but in onPress it's commented out

Copy link
Collaborator Author

@ethanshar ethanshar Nov 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed it in a following PR

const _onPress = useCallback(() => {
// onPress(_.isObject(value) ? value : {value, label});
onPress(value);
}, [value, onPress]);

getLabel() {
const {value, label} = this.props;
const onSelectedLayout = useCallback((...args) => {
_.invoke(props, 'onSelectedLayout', ...args);
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we pass props as a dependency?

Copy link
Collaborator Author

@ethanshar ethanshar Nov 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is internal usage.
onSelectedLayout is being sent from the Picker and it doesn't change..
we need it for first mount, so dependencies are irrelevant in this case..


const getLabel = () => {
if (_.isObject(value)) {
return _.invoke(this.props, 'getItemLabel', value) || _.get(value, 'label');
return _.invoke(props, 'getItemLabel', value) || _.get(value, 'label');
}
return label;
}

renderSelectedIndicator() {
const {
isSelected,
disabled,
selectedIcon = Assets.icons.check,
selectedIconColor = Colors.primary
} = this.props;
};

const selectedIndicator = useMemo(() => {
if (isSelected) {
return <Image source={selectedIcon} tintColor={disabled ? Colors.dark60 : selectedIconColor}/>;
}
}

renderItem() {
const {disabled} = this.props;
}, [isSelected, disabled, selectedIcon, selectedIconColor]);

const _renderItem = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also in here, should be memoized

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to overkill the component with memoization.
I read in a blog post about when to useMemo and it says it's not recommended to overuse memoization cause it the it can also affect performance.
From what I read, the thumb rule is to start simple and to consider memoizing when we face performance issues.

memoizing this will require wrapping getLabel with useCallback and I prefer not to complicate it at the moment.

Anyway, Personally I still not certain what is best practice with how often to use useMemo so again, for now I prefer to go easy on it..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. confusing topic indeed :)

return (
<View style={this.styles.container} flex row spread centerV>
<Text numberOfLines={1} style={[this.styles.labelText, disabled && this.styles.labelTextDisabled]}>
{this.getLabel()}
<View style={styles.container} flex row spread centerV>
<Text numberOfLines={1} style={[styles.labelText, disabled && styles.labelTextDisabled]}>
{getLabel()}
</Text>
{this.renderSelectedIndicator()}
{selectedIndicator}
</View>
);
}

onSelectedLayout = (...args) => {
_.invoke(this.props, 'onSelectedLayout', ...args);
};

render() {
const {renderItem, value, disabled, isSelected, testID} = this.props;

return (
<TouchableOpacity
activeOpacity={0.5}
onPress={this.onPress}
onLayout={isSelected ? this.onSelectedLayout : undefined}
disabled={disabled}
testID={testID}
throttleTime={0}
{...this.extractAccessibilityProps()}
>
{renderItem ? renderItem(value, this.props, this.getLabel()) : this.renderItem()}
</TouchableOpacity>
);
return (
<TouchableOpacity
activeOpacity={0.5}
onPress={_onPress}
onLayout={isSelected ? onSelectedLayout : undefined}
disabled={disabled}
testID={testID}
throttleTime={0}
{...Modifiers.extractAccessibilityProps(props)}
>
{renderItem ? renderItem(value, props, getLabel()) : _renderItem()}
</TouchableOpacity>
);
};

const styles = StyleSheet.create({
container: {
height: 56.5,
paddingHorizontal: 23,
borderColor: Colors.rgba(Colors.dark10, 0.1),
borderBottomWidth: 1
},
labelText: {
...Typography.text70,
color: Colors.dark10,
flex: 1,
textAlign: 'left'
},
labelTextDisabled: {
color: Colors.dark60
}

// TODO: deprecate the check for object
onPress = () => {
const {value, label, onPress} = this.props;
onPress(_.isObject(value) ? value : {value, label});
// onPress(value);
};
}

function createStyles() {
return StyleSheet.create({
container: {
height: 56.5,
paddingHorizontal: 23,
borderColor: Colors.rgba(Colors.dark10, 0.1),
borderBottomWidth: 1
},
labelText: {
...Typography.text70,
color: Colors.dark10,
flex: 1,
textAlign: 'left'
},
labelTextDisabled: {
color: Colors.dark60
}
});
}
});

PickerItem.displayName = 'Picker.Item';
PickerItem.propTypes = {
/**
* Item's value
*/
value: PropTypes.oneOfType([PropTypes.object, PropTypes.string, PropTypes.number]),
/**
* Item's label
*/
label: PropTypes.string,
/**
* Custom function for the item label (e.g (value) => customLabel)
*/
getItemLabel: PropTypes.func,
/**
* DEPRECATE: Function to return the value out of the item value prop when value is custom shaped.
*/
getItemValue: PropTypes.func,
/**
* Is the item selected
*/
isSelected: PropTypes.bool,
/**
* Pass to change the selected icon
*/
selectedIcon: PropTypes.oneOfType([PropTypes.object, PropTypes.number]),
/**
* Pass to change the selected icon color
*/
selectedIconColor: PropTypes.string,
/**
* Is the item disabled
*/
disabled: PropTypes.bool,
/**
* Render custom item
*/
renderItem: PropTypes.elementType,
/**
* Callback for onPress action
*/
onPress: PropTypes.func,
/**
* Callback for onLayout event
*/
onSelectedLayout: PropTypes.func
};

export default PickerItem;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PickerItem should be exported as memoized component

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due tho some of the of the props in PickerItem (like renderItem callback prop), it's very possible that memoizing the whole component might cause some side effects and break behaviors.
For now I think it will be better to keep parity with the previous implementation (where PickerItem was simple Component and not a pure one) and later on consider optimizations if necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood :)