Как добавить независимое состояние загрузки для каждого элемента списка?

#reactjs #react-native

#reactjs #react-native

Вопрос:

У меня есть экран способов оплаты с простым списком и кнопкой удаления для каждого элемента:

 const PaymentIndexScreen = ({ navigation }: Props): ReactElement => {
  const [creditCards, setCreditCards] = useState<CreditCard[]>([]);

  useEffect(() => navigation.addListener('focus', () => {
    client.service('credit-cards').find().then((result) => {
      setCreditCards(result.data.map((creditCardData) => {
        const year = creditCardData.expirationDate.slice(2);
        const month = creditCardData.expirationDate.slice(0, 2);

        return {
          ...creditCardData,
          expirationDate: `${month}/${year}`,
          loading: false,
        };
      }));
    });
  }), [navigation]);

  const handleDelete = (selectedCard): void => {
    console.log(selectedCard);
    selectedCard.loading = true;
    Alert.alert(
      'Delete?',
      null,
      [
        {
          text: 'Cancel'
        },
        {
          text: 'Delete',
          onPress: () => client.service('credit-cards').remove(selectedCard._id)
            .then(() => setCreditCards(creditCards.filter((card) => card._id !== selectedCard._id)))
            .catch(handleClientError)
          ,
        },
      ],
    );
  };

  return (
    <ScreenWrapper>
      <ScreenHeader
        title="Payment"
        navigation={navigation}
      />
      <ScrollView>
        <ScreenTitle label="Payment methods" />
        {creditCards.length > 0
          ? creditCards.map((card) => (
            <ListItem
              key={card._id}
              title={`●●●● ●●●● ●●●● ${card.lastDigits}`}
              content={`Expire on ${card.expirationDate}.`}
              onDelete={() => {handleDelete(card)}}
              disabled={card.loading}
            />
          ))
          : (
            <Banner
              iconName="credit-card"
              text="No payment method"
            />
          )}
      </ScrollView>
      <View>
        <Button
          title="Add"
          onPress={() => navigation.navigate('Add')}
        />
      </View>
    </ScreenWrapper>
  );
};

export default PaymentIndexScreen;
  

Действие удаления работает, за исключением изменения состояния загрузки.

Похоже, что изменение card.loading свойства запускает какой-либо рендеринг.

Единственное решение, которое я вижу, это полностью заменить созданный массив (как я сделал для удаления) включенным обновленным элементом, но я нашел это очень тяжелым и мог бы (нет?) приводит к проблемам с производительностью.

Каков наилучший способ реализовать независимое состояние загрузки без перезаписи всего массива?

Спасибо!

Ответ №1:

Из вашего кода мне кажется, что подход к обработке состояния списка нуждается в исправлении при возникновении некоторых событий. Я считаю, что возможным решением было бы абстрагироваться от эксклюзивной функции, которая может обрабатывать состояние каждой строки.

Пример (Песочница):

     const handleData = (item, newState) =>
    setData((prevData) =>
      prevData.map((prevItem) => {
        if (item.index === prevItem.index) return { ...prevItem, ...newState };
        return prevItem;
      })
    );
  

Комментарии:

1. Большое спасибо, песочница действительно помогла! Итак, нет другого выбора, кроме как переписать весь массив, понял.

2. Это простой подход к управлению состоянием. Существует другой подход к управлению состоянием с использованием Redux Immer ( github.com/immerjs/immer ), но я не думаю, что это будет полезно для вашего приложения прямо сейчас. Immer обрабатывает большую часть изменений дерева в состоянии под капотом.

Ответ №2:

React повторно отображает компонент при изменении его реквизитов или состояния. В вашем случае ни то, ни другое не меняется. Обратите внимание, что для того, чтобы react запускал изменения, состояние / реквизиты должны быть изменены неизменно.

В вашем случае вам нужно будет использовать setCreditCards для изменения вашего массива в вашей handleDelete функции.

 setCreditCards(
  creditCards.map(creditCard => {
    if (creditCard._id === selectedCard._id) {
      // return selectedCard with loading set to true
      return {
        ...selectedCard,
        loading: true,
      };
      // Return the card
    }
    return creditCard;
  }),
);
  

Обычно это не вызывает проблем с производительностью, если только ваш список не действительно большой, а временная сложность равна O (n). В этом случае вы, вероятно, в любом случае рассмотрите отложенную загрузку или загрузку частичных данных.