change TextInput value other component will re render again although I wrap it in memo() and useCallback()

56 views Asked by At

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.

1

There are 1 answers

0
Dhruv Tailor On BEST ANSWER

The ButtonFlag Component will re-render because you are passing an object in the item's prop. On every render of the PhoneInput 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 the useMemo hook.

To memoize the renderDropDown, you can create a separate component for dropdown, just like you created for the ButtonFlag and use then use memo.