This is my PhoneInput Component. I use useTheme from react native boilerplate and useController for form
import { ButtonFlag, Icon, ImageVariant, TextInput } from '@/components/atoms';
import CountryFlag from '@/components/atoms/IconFlag/IconFlag';
import { countries } from '@/constants';
import { useTheme } from '@/theme';
import { dimensionSize } from '@/theme/_config';
import phones from '@/utils/phones';
import valid from '@/utils/validator';
import { memo, useCallback, useEffect, useState } from 'react';
import { useController } from 'react-hook-form';
import {
FlatList,
Pressable,
StyleProp,
Text,
TouchableOpacity,
View,
ViewStyle,
} from 'react-native';
type Props = {
style?: StyleProp<ViewStyle>;
name: string;
control: any;
};
function PhoneInput({ control, name, style }: Props) {
const { field, fieldState } = useController({ control, name });
const { layout, gutters, borders, backgrounds, fonts, colors } = useTheme();
const [open, setOpen] = useState(false);
const [value, setValue] = useState(countries[0].value);
const [phone, setPhone] = useState('');
console.log('render PhoneInput============');
useEffect(() => {
if (value && phone) {
const validPhone = valid.checkValidPhone(phone);
if (validPhone) {
field.onChange(
phones.formatPhoneNumber(value.isoCode, phone, {
format: 'E.164',
}) || ''
);
}
}
}, [value, phone]);
const toggleDropdown = useCallback(() => {
setOpen((prevOpen) => !prevOpen);
}, []);
const handlePhoneChange = useCallback((text: string) => {
setPhone(text);
}, []);
const renderDropDown = useCallback(() => {
console.log('render list');
if (open) {
return (
<View
style={[
{
zIndex: 1,
position: 'absolute',
top: 6 + 46 + 1,
width: '100%',
borderWidth: 1,
},
backgrounds.white,
borders.rounded_6,
borders.grey,
gutters.paddingHorizontal_15,
]}
>
<FlatList
data={countries}
renderItem={({ item, index }) => {
return (
<Pressable
onPress={() => {
setValue(item.value);
toggleDropdown();
}}
style={({ pressed }) => {
return [
{
flexDirection: 'row',
borderTopWidth: index !== 0 ? 1 : 0,
transform: [{ scale: pressed ? 0.98 : 1 }],
height: 46,
},
layout.itemsCenter,
borders.grey,
];
}}
>
<CountryFlag isoCode={item.value.isoCode} size={22} />
<Text
style={[
gutters.marginLeft_10,
fonts.black300,
fonts.size_16,
fonts.weight400,
]}
>
{item.label}
</Text>
{value === item.value && (
<Icon
style={{
position: 'absolute',
right: 0,
}}
color={colors.pink600}
name="checked"
size={20}
/>
)}
</Pressable>
);
}}
/>
</View>
);
}
}, [open, toggleDropdown, value]);
return (
<View
style={{
zIndex: 1,
}}
>
<View style={style ? style : undefined}>
<View
style={[
layout.row,
{
overflow: 'hidden',
},
]}
>
<ButtonFlag
isError={fieldState.invalid}
isOpen={open}
item={{
countryNumber: value.phoneCode,
ISOcode: value.isoCode,
}}
onPress={toggleDropdown}
/>
<View
style={[
{
flexGrow: 1,
position: 'relative',
borderWidth: 1,
height: 46,
flex: 1,
},
gutters.marginLeft_10,
borders.rounded_6,
fieldState.invalid ? borders.red : borders.grey,
layout.row,
layout.itemsCenter,
gutters.paddingHorizontal_10,
]}
>
<TextInput
keyboardType="decimal-pad"
value={phone}
onChangeText={handlePhoneChange}
placeholder=""
/>
{phone && (
<TouchableOpacity
onPress={() => {
handlePhoneChange('');
}}
>
<Icon name="clear" color="grey" size={25} />
</TouchableOpacity>
)}
</View>
</View>
<View
style={{
width: dimensionSize.screen.width - 20 * 2,
minHeight: 21,
}}
>
{fieldState.invalid && (
<Text
numberOfLines={3}
style={[
fonts.red,
fonts.size_14,
fonts.weight400,
{ flexWrap: 'wrap' },
]}
>{`${fieldState.error?.message}`}</Text>
)}
</View>
{renderDropDown()}
</View>
</View>
);
}
export default memo(PhoneInput);
import { Pressable, Text, View } from 'react-native';
import { IconFlag } from '@/components/atoms';
import { useTheme } from '@/theme';
import { memo } from 'react';
type FlagProps = {
ISOcode: string;
countryNumber: number;
};
type Props = {
onPress: () => void;
item: FlagProps;
isOpen: boolean;
isError: boolean;
};
function ButtonFlag({ onPress, item, isOpen, isError }: Props) {
const { gutters, layout, borders, fonts } = useTheme();
const { countryNumber, ISOcode } = item;
console.log('render button');
return (
<Pressable
style={({ pressed }) => {
return [
layout.row,
layout.itemsCenter,
{
paddingHorizontal: 15,
height: 46,
borderWidth: 1,
transform: [{ scale: pressed ? 0.98 : 1 }],
},
borders.rounded_6,
isOpen ? borders.pink600 : isError ? borders.red : borders.grey,
];
}}
onPress={onPress}
>
<IconFlag isoCode={ISOcode} size={23} />
<Text
style={[
gutters.marginLeft_10,
fonts.black300,
fonts.weight400,
fonts.size_16,
]}
>{`+${countryNumber}`}</Text>
</Pressable>
);
}
export default memo(ButtonFlag);
This is my ButtonFlag component
When I change textinput value, other component like renderDropdown and BUttonFlag still re render. Thank you if anyone can help me . I new with useCallback and memo. Very pleasure to fix and learn more.
The
ButtonFlag
Component will re-render because you are passing an object in the item's prop. On every render of thePhoneInput
Component, a new object will be passed as a prop to ButtonFlag and in javascript,{} === {}
is always false, so previous props and new props will always be different.And for
renderDropDown
function,useCallback
will cache the definition of the function and not the value of the function. To cache the value of the function, you should use theuseMemo
hook.To memoize the
renderDropDown
, you can create a separate component for dropdown, just like you created for the ButtonFlag and use then usememo
.