Kevin Jayden Wivano
Kevin Jayden Wivano

Reputation: 346

React Native : [Unhandled promise rejection: Error: Invalid hook call. Hooks can only be called inside of the body of a function component

I am new to React Native, and this is my first project using React Native that build from scratch. I have a warning issue and the feature i want is not working.

Here are some explanation of what i want : I have an array of object that i named it as carts, inside the carts it looks like :

let carts = [
  {
    merchantId: 1,
    items: [
      {
        productId: 1,
        productQty: 1,
      },
      {
        productId: 2,
        productQty: 5,
      },
    ],
  },
  {
    merchantId: 2,
    items: [
      {
        productId: 3,
        productQty: 2,
      },
      {
        productId: 4,
        productQty: 4,
      },
    ],
  },
];

And this is my Screen :

import React, { useState, useEffect } from 'react';
import {
  StyleSheet,
  Text,
  View,
  Image,
  ScrollView,
  TouchableOpacity,
} from 'react-native';
import { Button } from 'react-native-paper';
import { theme } from '../../../infrastructure/theme';
import SpacerComponent from '../../../components/SpacerComponent';
import { FontAwesome5 } from '@expo/vector-icons';
import MerchantCardComponent from '../components/MerchantCardComponent';
import { tranformCartArray } from '../../../services/carts/CartTransform';

const CartScreen = ({ navigation }) => {
  const [carts, setCarts] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  const getCarts = async () => {
    setIsLoading(true);
    const response = await tranformCartArray();
    setCarts(response);
    setIsLoading(false);
  };

  useEffect(() => {
    const unsubscribe = navigation.addListener('focus', () => {
      getCarts();
    });

    return unsubscribe;
  }, [navigation]);

  console.log(carts);

  if (carts.length === 0) {
    return (
      <View style={styles.emptyContainter}>
        <Image
          style={styles.imageCart}
          source={require('../../../assets/images/empty_cart.png')}
        />
        <Text style={styles.emptyCartText}>
          Keranjangnya masih kosong nih !
        </Text>
        <SpacerComponent size="xlarge" />
        <Button
          style={styles.buttonExplore}
          mode="contained"
          size={20}
          onPress={() => navigation.navigate('Merchants')}
        >
          <FontAwesome5
            name="shopping-cart"
            size={20}
            color={theme.colors.text.inverse}
          />
          <Text>{'   '}</Text>
          <Text style={styles.buttonText}>Eksplor</Text>
        </Button>
      </View>
    );
  }

  if (isLoading) {
    return (
      <View style={styles.emptyContainter}>
        <ActivityIndicator
          color={theme.colors.ui.primary}
          size={theme.sizes[3]}
        />
      </View>
    );
  }

  return (
    <ScrollView style={styles.container}>
      {carts.map((merchantAndItems) => {
        return (
          <TouchableOpacity onPress={() => navigation.navigate('Checkout')}>
            <MerchantCardComponent
              merchantAndItems={merchantAndItems}
              key={merchantAndItems.merchantId}
            />
          </TouchableOpacity>
        );
      })}
    </ScrollView>
  );
};

export default CartScreen;

const styles = StyleSheet.create({
  emptyContainter: {
    justifyContent: 'center',
    alignItems: 'center',
    flex: 1,
    backgroundColor: theme.colors.bg.primary,
  },
  imageCart: {
    maxWidth: '90%',
    maxHeight: '45%',
    aspectRatio: 1,
  },
  emptyCartText: {
    fontSize: 20,
    fontFamily: theme.fonts.regular,
  },
  buttonExplore: {
    backgroundColor: theme.colors.ui.primary,
  },
  buttonText: {
    fontSize: 20,
  },
  container: {
    flex: 1,
    marginBottom: 10,
  },
});

I want to pass after transform carts of array to MerchantCardComponent, the type of the transformation I want is the array (carts) become :

let carts = [
  {
    merchantId: 1,
    merchantName: 'Merchant A',
    merchantLogo: 'https://assets.backend.com/merchants/1/logo_url.jpg',
    items: [
      {
        productId: 1,
        productName: 'Product One',
        productImage: 'https://assets.backend.com/merchants/1/products/1/product_image.jpg',
        productPrice: 2000,
        productQty: 1,
      },
      {
        productId: 2,
        productName: 'Product Two',
        productImage: 'https://assets.backend.com/merchants/1/products/2/product_image.jpg',
        productPrice: 2000,
        productQty: 5,
      },
    ],
  },
  {
    merchantId: 2,
    merchantName: 'Merchant B',
    merchantLogo: 'https://assets.backend.com/merchants/2/logo_url.jpg',
    items: [
      {
        productId: 3,
        productName: 'Product Three',
        productImage: 'https://assets.backend.com/merchants/2/products/3/product_image.jpg',
        productPrice: 2000,
        productQty: 2,
      },
      {
        productId: 4,
        productName: 'Product Four',
        productImage: 'https://assets.backend.com/merchants/4/products/4/product_image.jpg',
        productPrice: 2000,
        productQty: 4,
      },
    ],
  },
];

So that i can easily just print out the text, without fetch to backend again in the screen. I have make an seperate file for the transform the cart function, here is the code :

import React, { useContext, useState } from 'react';
import CartsContext from './CartsContext';

import {
  doRequestMerchantDetail,
  doRequestProductDetail,
} from '../merchants/MerchantsService';

export const tranformCartArray = async () => {
  const [transformCart, setTransformCart] = useState([]);
  const [afterTransform, setAfterTransform] = useState(null);
  const [items, setItems] = useState([]);
  const { carts } = useContext(CartsContext);

  for (let i = 0; i < carts.length; i++) {
    let [errMerchant, merchant] = await doRequestMerchantDetail(
      carts[i].merchantId,
    );

    for (let j = 0; j < cart[i].items.length; j++) {
      let [errProduct, product] = await doRequestProductDetail(
        carts[i].items[j].productId,
      );
      if (product) {
        setItems(
          ...items,
          ...[
            {
              productName: product.product_name,
              productQty: carts[i].items[j].productQty,
              productPrice: product.product_price,
              productId: carts[i].items[j].productId,
              productImage: product.product_image_url,
            },
          ],
        );
      } else {
        setItems(
          ...items,
          ...[
            {
              productName: 'Error',
              productQty: carts[i].items[j].productQty,
              productPrice: 'Error',
              productId: carts[i].items[j].productId,
              productImage:
                'https://cdn.pixabay.com/photo/2021/07/21/12/49/error-6482984_960_720.png',
            },
          ],
        );
      }
    }

    if (merchant) {
      setAfterTransform({
        merchantName: merchant.name,
        merchantLogo: merchant.merchant_logo_url,
        merchantId: carts[i].merchantId,
        items,
      });
    } else {
      setAfterTransform({
        merchantName: 'Error',
        merchantLogo: 'Error',
        merchantId: carts[i].merchantId,
        items,
      });
    }

    setTransformCart(...transformCart, ...[afterTransform]);
  }

  return transformCart;
};

Until now i'm still confuse what make the warning show out, and the not working i was mention in the first sentence is the it return an empty array after the transform. Here is the screenshoot of console ConsoleLog in Screen

Upvotes: 3

Views: 280

Answers (1)

Drew Reese
Drew Reese

Reputation: 202667

tranformCartArray isn't a react component or custom React hook so you can't use the useState and useContext hooks.

Additionally, the "unhandled rejection" was thrown because the getCarts function is declared async so it implicitly returns a Promise and when the React hooks error was thrown there was no catch block or .catch of a Promise chain to catch and handle it.

I would suggest passing the carts state from the context into the tranformCartArray utility function, and return an augmented carts array. This allows you to remove any and all hooks from the function.

export const tranformCartArray = async (carts) => {
  const newCarts = [];

  for (let i = 0; i < carts.length; i++) {
    let [, merchant] = await doRequestMerchantDetail(
      carts[i].merchantId,
    );
    
    const newItems = [];

    for (let j = 0; j < carts[i].items.length; j++) {
      let [, product] = await doRequestProductDetail(
        carts[i].items[j].productId,
      );

      newItems.push({
        productName: product?.product_name ?? 'Error',
        productQty: carts[i].items[j].productQty,
        productPrice: product?.product_price ?? 'Error',
        productId: carts[i].items[j].productId,
        productImage: carts[i].items[j].product_image_url,
      });
    }
    
    newCarts.push({
      merchantName: merchant?.name ?? 'Error',
      merchantLogo: merchant?.merchant_logo_url ?? 'Error',
      merchantId: carts[i].merchantId,
      items: newItems,
    });
  }

  return newCarts;
};

CartScreen

const CartScreen = ({ navigation }) => {
  const [carts, setCarts] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  const { carts: contextCarts } = useContext(CartsContext); // <-- access context

  const getCarts = async () => {
    setIsLoading(true);
    try {
      const response = await tranformCartArray(contextCarts); // <-- pass context carts value
      setCarts(response);
    } catch(error) {
      // handle any errors?
    } finally {
      setIsLoading(false);
    }
  };

  ...

Upvotes: 3

Related Questions