Search code examples
reactjsreact-nativereact-hooksuse-effectuse-state

Chaining React.useEffect dependencies results in slow re-rendering


I am creating a screen to "buy" crypto, and I have organized the components as in the image attached. When I change the EUR input amount I expect the calculations to be done very fast, since no API calls are involved (just basic math operations). Since I am passing the useEffect state and update function down from BuyScreen to CurrencyWidget and FeeSummary as props, I need to use (as far as I know) useEffect and a dependency on a value to make sure I have the last updated one.

The issue consists in how slow the UI is updating (see GIF), since I am changing several useEffect hooks to be sure I have all the updated values. Is there something that can be improved in my code to solve this issue?

components hierarchy

BuyScreen.js

const calculateTotalBuyTransactionAmount = (
  buySourceAmount,
  cryptoBuyPrice
) => {
  return (buySourceAmount / cryptoBuyPrice).toFixed(8);
};

const calculateSubtotal = (amount, fees) => {
  return (amount - fees).toFixed(2);
};

const BuyScreen = (props) => {
  const [buySourceAmount, setBuySourceAmount] = useState(0.0);
  const [buyDestinationAmount, setBuyDestinationAmount] = useState(0.0);
  const [feeAmount, setFeeAmount] = useState(0.0);
  const [feeSubTotalAmount, setFeeSubTotalAmount] = useState(0.0);
  const [cryptoBuyPrice, setCryptoBuyPrice] = useState(
    pricesMock[0].values.prices.EUR.buy
  );

  useEffect(() => {
    setFeeAmount((buySourceAmount * 0.05).toFixed(2));
  }, [buySourceAmount]);

  useEffect(() => {
    setFeeSubTotalAmount(calculateSubtotal(buySourceAmount, feeAmount));
  }, [feeAmount]);

  useEffect(() => {
    setBuyDestinationAmount(
      calculateTotalBuyTransactionAmount(feeSubTotalAmount, cryptoBuyPrice)
    );
  }, [feeSubTotalAmount]);

  console.log("Rendering BuyScreen");

  return (
    <SafeAreaView style={{ flex: 1 }}>
      <Container>
        <Content padder>
          <View>
            <CurrencyWidget
              currencyName={balancesMock.values.currencyBalances[0].name}
              currencyCode={balancesMock.values.currencyBalances[0].code}
              balance={balancesMock.values.currencyBalances[0].total}
              inputAmount={buySourceAmount}
              setInputAmount={(amount) => {
                setBuySourceAmount(amount);
              }}
              autofocus
            />
            <FeesSummary
              feeAmount={feeAmount}
              feeSubTotalAmount={feeSubTotalAmount}
            />
            <RealTimeCryptoPriceWidget
              currencyCode={balancesMock.values.cryptoBalances[0].code}
              cryptoBuyPrice={cryptoBuyPrice}
            />
            <CurrencyWidget
              currencyName={balancesMock.values.cryptoBalances[0].name}
              currencyCode={balancesMock.values.cryptoBalances[0].code}
              balance={balancesMock.values.cryptoBalances[0].total}
              inputAmount={buyDestinationAmount}
              destinationCurrency
            />
          </View>
          <View>
            <LoadingSpinner area="buy-button">
              <Button
                block
                primary-light
                style={{
                  marginBottom: 16,
                }}
              >
                <Text>Buy {balancesMock.values.cryptoBalances[0].name}</Text>
              </Button>
            </LoadingSpinner>
          </View>
        </Content>
      </Container>
    </SafeAreaView>
  );
};

CurrencyWidget.js

const CurrencyWidget = (props) => {
  console.log("Rendering CurrencyWidget");
  return (
    <Grid>
      <Col>
        <Text notification-light>
          {props.currencyName} ({props.currencyCode})
        </Text>
        <Text label-light style={styles.balanceAmount}>
          Balance: {props.currencyCode} {props.balance}
        </Text>
      </Col>
      <Col style={{ alignItems: "flex-end", justifyContent: "flex-start" }}>
        <Item regular>
          <Input
            transactionAmount
            keyboardType="numeric"
            placeholder="0"
            editable={!props.destinationCurrency}
            autoFocus={props.autofocus}
            value={String(props.inputAmount)}
            onChangeText={(amount) => {
              console.log("amount: ", amount);
              amount === ""
                ? props.setInputAmount("0")
                : props.setInputAmount(amount);
            }}
          />
        </Item>
      </Col>
    </Grid>
  );
};

FeeSummary.js

const FeesSummary = props => {
  const sellPageFees = () => {
    return (
      <Grid style={styles.verticalPadding}>
        <Row>
          <Col style={styles.verticalCenter}>
            <Text label-light>Subtotal</Text>
          </Col>
          <Col style={[styles.verticalCenter, { alignItems: "flex-end" }]}>
            <Text notification-light-regular>
              EUR {props.feeSubTotalAmount}
            </Text>
          </Col>
        </Row>
        <Row>
          <Col style={styles.verticalCenter}>
            <Text label-light>Fees</Text>
          </Col>
          <Col style={[styles.verticalCenter, { alignItems: "flex-end" }]}>
            <Text notification-light-regular>EUR {props.feeAmount}</Text>
          </Col>
        </Row>
      </Grid>
    );
  };

  const buyPageFees = () => {
    return (
      <Grid style={styles.verticalPadding}>
        <Row>
          <Col style={styles.verticalCenter}>
            <Text label-light>Fees</Text>
          </Col>
          <Col style={[styles.verticalCenter, { alignItems: "flex-end" }]}>
            <Text notification-light-regular>EUR {props.feeAmount}</Text>
          </Col>
        </Row>
        <Row>
          <Col style={styles.verticalCenter}>
            <Text label-light>Subtotal</Text>
          </Col>
          <Col style={[styles.verticalCenter, { alignItems: "flex-end" }]}>
            <Text notification-light-regular>
              EUR {props.feeSubTotalAmount}
            </Text>
          </Col>
        </Row>
      </Grid>
    );
  };
  console.log("Rendering FeeSummary");
  return props.isSellPage ? sellPageFees() : buyPageFees();
};

Solution

  • For anyone running in the same issue, I managed to solve by handling the state update separately from the calculations needed to update the fields, see the following code:

    BuyScreen.js

    <CurrencyWidget
       currencyName={balancesMock.values.currencyBalances[0].name}
       currencyCode={balancesMock.values.currencyBalances[0].code}
       balance={balancesMock.values.currencyBalances[0].total}
       inputAmount={buySourceAmount}
       setInputAmount={handleBuySourceAmount}
       autofocus
    />
    

    And the handleBuySourceAmount function:

    const handleBuySourceAmount = (amount) => {
        const _feeAmount = calculateFees(amount);
        const _subTotalAmount = calculateSubtotal(amount, _feeAmount);
        const _totalBuyAmount = calculateTotalBuyTransactionAmount(
          _subTotalAmount,
          cryptoBuyPrice
        );
    
        setBuySourceAmount(amount);
        setFeeAmount(_feeAmount);
        setFeeSubTotalAmount(_subTotalAmount);
        setBuyDestinationAmount(_totalBuyAmount);
      };