diff --git a/packages/desktop-client/e2e/mobile.test.js b/packages/desktop-client/e2e/mobile.test.js
index fa53ecd515283c71ec8ecc43d562266f04186487..b67a03e42437f9d62c01efb6a2767f531666bab5 100644
--- a/packages/desktop-client/e2e/mobile.test.js
+++ b/packages/desktop-client/e2e/mobile.test.js
@@ -83,6 +83,8 @@ test.describe('Mobile', () => {
     await expect(transactionEntryPage.header).toHaveText('New Transaction');
 
     await transactionEntryPage.amountField.fill('12.34');
+    // Click anywhere to cancel active edit.
+    await transactionEntryPage.header.click();
     await transactionEntryPage.fillField(
       page.getByTestId('payee-field'),
       'Kroger',
@@ -114,6 +116,8 @@ test.describe('Mobile', () => {
     await expect(page).toMatchThemeScreenshots();
 
     await transactionEntryPage.amountField.fill('12.34');
+    // Click anywhere to cancel active edit.
+    await transactionEntryPage.header.click();
     await transactionEntryPage.fillField(
       page.getByTestId('payee-field'),
       'Kroger',
diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png
index 109397af4d87b9d517562e55791d9301490c205d..71c34be4250eb6f6bab7ca82e2b43ef17915e459 100644
Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png
index d0990083a9997d3b05cb0a02ed22ace0d28f1d7e..885ad0401f9cb1d33aaf9feb42ff44da1f8b09c2 100644
Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png
index 98d8921bfd4263ab933679ad3067d6ae7645b2ac..f66759c3186679ff08022e4ff6f7a8fce7bdd71a 100644
Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png
index ff9a2444f99745d1cfdb67660be71dd7bd4e1b38..3f118cdc0acbe3b5f4fe4a92adc82f0bbfbc8133 100644
Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png
index 27df8c3e591cd3da4e725d0a204947c4a16edf8a..3404a4f5b328384777d44604afab43d4522e7e08 100644
Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-4-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-4-chromium-linux.png
index ff627b5a0a609de8a653f4b376ae45c30e089295..6bb639fa3de5981100833af0036f80ddf9633c6f 100644
Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-4-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-4-chromium-linux.png differ
diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx
index 6c9238faf2a6e537ef4daad53640afc5d7223653..8f4fadba8363db7cf2a811cab79c938aa14386a9 100644
--- a/packages/desktop-client/src/components/Modals.tsx
+++ b/packages/desktop-client/src/components/Modals.tsx
@@ -232,6 +232,7 @@ export default function Modals() {
               modalProps={modalProps}
               name={options.name}
               onSubmit={options.onSubmit}
+              onClose={options.onClose}
             />
           );
 
diff --git a/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx b/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx
index 4f8e81107c107377589bf384108759ae42632065..eb7905bd8dfc5179fc30ddf194c83c7de17b8233 100644
--- a/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx
+++ b/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx
@@ -151,7 +151,7 @@ function BudgetCell({
   return (
     <View style={style}>
       <AmountInput
-        initialValue={sheetValue}
+        value={sheetValue}
         zeroSign="+"
         style={{
           ...(!isEditing && { display: 'none' }),
@@ -160,7 +160,7 @@ function BudgetCell({
         }}
         focused={isEditing}
         textStyle={{ ...styles.smallText, ...textStyle }}
-        onChange={updateBudgetAmount}
+        onUpdate={updateBudgetAmount}
         onBlur={() => onEdit?.(null)}
       />
       <View
diff --git a/packages/desktop-client/src/components/mobile/MobileAmountInput.jsx b/packages/desktop-client/src/components/mobile/MobileAmountInput.jsx
index 4155bffab6afd235df165f144c9e4af1b20e680f..207912155ff9ca92d9d6231c8fcfe30439d71e5f 100644
--- a/packages/desktop-client/src/components/mobile/MobileAmountInput.jsx
+++ b/packages/desktop-client/src/components/mobile/MobileAmountInput.jsx
@@ -1,4 +1,4 @@
-import { PureComponent } from 'react';
+import { memo, useEffect, useRef, useState } from 'react';
 
 import {
   toRelaxedNumber,
@@ -11,302 +11,218 @@ import Button from '../common/Button';
 import Text from '../common/Text';
 import View from '../common/View';
 
-function getValue(state) {
-  const { value } = state;
-  return value;
-}
-
-class AmountInput extends PureComponent {
-  static getDerivedStateFromProps(props, state) {
-    return { editing: state.text !== '' || state.editing };
-  }
-
-  constructor(props) {
-    super(props);
-    // this.backgroundValue = new Animated.Value(0);
-    this.backgroundValue = 0;
-
-    this.id = Math.random().toString().slice(0, 5);
-    this.state = {
-      editing: false,
-      text: '',
-      // These are actually set from the props when the field is
-      // focused
-      value: 0,
-    };
-  }
-
-  componentDidMount() {
-    if (this.props.focused) {
-      this.focus();
+const AmountInput = memo(function AmountInput({
+  focused,
+  style,
+  textStyle,
+  ...props
+}) {
+  const [editing, setEditing] = useState(false);
+  const [text, setText] = useState('');
+  const [value, setValue] = useState(0);
+  const inputRef = useRef();
+
+  const getInitialValue = () => Math.abs(props.value);
+
+  useEffect(() => {
+    if (focused) {
+      focus();
     }
-  }
+  }, []);
 
-  componentWillUnmount() {
-    if (this.removeListeners) {
-      this.removeListeners();
-    }
-  }
-
-  componentDidUpdate(prevProps, prevState) {
-    if (!prevProps.focused && this.props.focused) {
-      this.focus();
-    }
+  useEffect(() => {
+    setEditing(text !== '');
+  }, [text]);
 
-    if (prevProps.value !== this.props.value) {
-      this.setState({
-        editing: false,
-        text: '',
-        ...this.getInitialValue(),
-      });
+  useEffect(() => {
+    if (focused) {
+      focus();
     }
-  }
+  }, [focused]);
 
-  parseText() {
-    return toRelaxedNumber(
-      this.state.text.replace(/[,.]/, getNumberFormat().separator),
-    );
-  }
+  useEffect(() => {
+    setEditing(false);
+    setText('');
+    setValue(getInitialValue());
+  }, [props.value]);
 
-  // animate() {
-  //   this.animation = Animated.sequence([
-  //     Animated.timing(this.backgroundValue, {
-  //       toValue: 1,
-  //       duration: 1200,
-  //       useNativeDriver: true,
-  //     }),
-  //     Animated.timing(this.backgroundValue, {
-  //       toValue: 0,
-  //       duration: 1200,
-  //       useNativeDriver: true,
-  //     }),
-  //   ]);
-
-  //   this.animation.start(({ finished }) => {
-  //     if (finished) {
-  //       this.animate();
-  //     }
-  //   });
-  // }
+  const parseText = () => {
+    return toRelaxedNumber(text.replace(/[,.]/, getNumberFormat().separator));
+  };
 
-  onKeyPress = e => {
-    if (e.nativeEvent.key === 'Backspace' && this.state.text === '') {
-      this.setState({ editing: true });
+  const onKeyPress = e => {
+    if (e.key === 'Backspace' && text === '') {
+      setEditing(true);
     }
   };
 
-  getInitialValue() {
-    return {
-      value: Math.abs(this.props.value),
-    };
-  }
-
-  focus() {
-    this.input.focus();
-
-    const initialState = this.getInitialValue();
-    this.setState(initialState);
-  }
-
-  applyText = () => {
-    const { editing } = this.state;
+  const focus = () => {
+    inputRef.current?.focus();
+    setValue(getInitialValue());
+  };
 
-    const parsed = this.parseText();
-    const newValue = editing ? parsed : getValue(this.state);
+  const applyText = () => {
+    const parsed = parseText();
+    const newValue = editing ? parsed : value;
 
-    this.setState({
-      value: Math.abs(newValue),
-      editing: false,
-      text: '',
-    });
+    setValue(Math.abs(newValue));
+    setEditing(false);
+    setText('');
 
     return newValue;
   };
 
-  onBlur = () => {
-    const value = this.applyText();
-    this.props.onBlur?.(value);
-    if (this.removeListeners) {
-      this.removeListeners();
-    }
+  const onBlur = () => {
+    const value = applyText();
+    props.onUpdate?.(value);
   };
 
-  onChangeText = text => {
-    const { onChange } = this.props;
-
-    this.setState({ text });
-    onChange(text);
+  const onChangeText = text => {
+    setText(text);
+    props.onChange?.(text);
   };
 
-  render() {
-    const { style, textStyle } = this.props;
-    const { editing, value, text } = this.state;
-
-    const input = (
-      <input
-        type="text"
-        ref={el => (this.input = el)}
-        value={text}
-        inputMode="decimal"
-        autoCapitalize="none"
-        onChange={e => this.onChangeText(e.target.value)}
-        onBlur={this.onBlur}
-        onKeyPress={this.onKeyPress}
-        data-testid="amount-input"
-        style={{ flex: 1, textAlign: 'center', position: 'absolute' }}
-      />
-    );
-
-    return (
-      <View
-        style={{
-          justifyContent: 'center',
-          borderWidth: 1,
-          borderColor: theme.pillBorderSelected,
-          borderRadius: 4,
-          padding: 5,
-          backgroundColor: theme.tableBackground,
-          ...style,
-        }}
+  const input = (
+    <input
+      type="text"
+      ref={inputRef}
+      value={text}
+      inputMode="decimal"
+      autoCapitalize="none"
+      onChange={e => onChangeText(e.target.value)}
+      onBlur={onBlur}
+      onKeyUp={onKeyPress}
+      data-testid="amount-input"
+      style={{ flex: 1, textAlign: 'center', position: 'absolute' }}
+    />
+  );
+
+  return (
+    <View
+      style={{
+        justifyContent: 'center',
+        borderWidth: 1,
+        borderColor: theme.pillBorderSelected,
+        borderRadius: 4,
+        padding: 5,
+        backgroundColor: theme.tableBackground,
+        ...style,
+      }}
+    >
+      <View style={{ overflowY: 'auto', overflowX: 'hidden' }}>{input}</View>
+      <Text
+        style={textStyle}
+        data-testid="amount-fake-input"
+        pointerEvents="none"
       >
-        <View style={{ overflowY: 'auto', overflowX: 'hidden' }}>{input}</View>
-
-        {/* <Animated.View
-          style={{
-            position: 'absolute',
-            left: 0,
-            right: 0,
-            bottom: 0,
-            top: 0,
-
-            backgroundColor: animationColor || colors.p10,
-            opacity: this.backgroundValue,
-            borderRadius: 2,
-          }}
-          pointerEvents="none"
-        /> */}
-        <Text
-          style={textStyle}
-          data-testid="amount-fake-input"
-          pointerEvents="none"
-        >
-          {editing ? text : amountToCurrency(value)}
-        </Text>
-      </View>
-    );
-  }
-}
-
-export class FocusableAmountInput extends PureComponent {
-  state = { focused: false, isNegative: true };
-
-  componentDidMount() {
-    if (this.props.sign) {
-      this.setState({ isNegative: this.props.sign === 'negative' });
-    } else if (
-      this.props.value > 0 ||
-      (!this.props.zeroIsNegative && this.props.value === 0)
-    ) {
-      this.setState({ isNegative: false });
+        {editing ? text : amountToCurrency(value)}
+      </Text>
+    </View>
+  );
+});
+
+export const FocusableAmountInput = memo(function FocusableAmountInput({
+  value,
+  sign, // + or -
+  zeroSign, // + or -
+  focused,
+  textStyle,
+  style,
+  focusedStyle,
+  buttonProps,
+  onFocus,
+  ...props
+}) {
+  const [isNegative, setIsNegative] = useState(true);
+
+  useEffect(() => {
+    if (sign) {
+      setIsNegative(sign === '-');
+    } else if (value > 0 || (zeroSign !== '-' && value === 0)) {
+      setIsNegative(false);
     }
-  }
-
-  focus = () => {
-    this.setState({ focused: true });
-  };
+  }, []);
 
-  onFocus = () => {
-    this.focus();
+  const toggleIsNegative = () => {
+    setIsNegative(!isNegative);
+    props.onUpdate?.(maybeApplyNegative(value, !isNegative));
   };
 
-  toggleIsNegative = () => {
-    this.setState({ isNegative: !this.state.isNegative }, () => {
-      this.onBlur(this.props.value);
-    });
+  const maybeApplyNegative = (val, negative) => {
+    const absValue = Math.abs(val);
+    return negative ? -absValue : absValue;
   };
 
-  maybeApplyNegative = value => {
-    const absValue = Math.abs(value);
-    return this.state.isNegative ? -absValue : absValue;
+  const onUpdate = val => {
+    props.onUpdate?.(maybeApplyNegative(val, isNegative));
   };
 
-  onBlur = value => {
-    this.setState({ focused: false, reallyFocused: false });
-    this.props.onBlur?.(this.maybeApplyNegative(value));
+  const onChange = val => {
+    props.onChange?.(maybeApplyNegative(val, isNegative));
   };
 
-  onChange = value => this.props.onChange?.(this.maybeApplyNegative(value));
-
-  render() {
-    const { textStyle, style, focusedStyle, buttonProps } = this.props;
-    const { focused } = this.state;
+  return (
+    <View>
+      <AmountInput
+        {...props}
+        value={value}
+        onChange={onChange}
+        onUpdate={onUpdate}
+        focused={focused}
+        style={{
+          width: 80,
+          justifyContent: 'center',
+          ...style,
+          ...focusedStyle,
+          ...(!focused && {
+            display: 'none',
+          }),
+        }}
+        textStyle={{ fontSize: 15, textAlign: 'right', ...textStyle }}
+      />
 
-    return (
       <View>
-        <AmountInput
-          {...this.props}
-          ref={el => (this.amount = el)}
-          onChange={this.onChange}
-          onBlur={this.onBlur}
-          focused={focused}
-          style={{
-            width: 80,
-            transform: [{ translateX: 6 }],
-            justifyContent: 'center',
-            ...style,
-            ...focusedStyle,
-            ...(!focused && {
-              opacity: 0,
-              position: 'absolute',
-              top: 0,
-            }),
-          }}
-          textStyle={{ fontSize: 15, textAlign: 'right', ...textStyle }}
-        />
-
-        <View>
-          {!focused && (
-            <Button
-              style={{
-                position: 'absolute',
-                right: 'calc(100% + 5px)',
-                top: '8px',
-              }}
-              onClick={this.toggleIsNegative}
-            >
-              {this.state.isNegative ? '−' : '+'}
-            </Button>
-          )}
+        {!focused && (
           <Button
-            onClick={this.onFocus}
-            // Defines how far touch can start away from the button
-            // hitSlop={{ top: 5, bottom: 5, left: 5, right: 5 }}
-            {...buttonProps}
             style={{
-              ...(buttonProps && buttonProps.style),
-              ...(focused && { display: 'none' }),
-              ':hover': {
-                backgroundColor: 'transparent',
-              },
+              position: 'absolute',
+              right: 'calc(100% + 5px)',
+              top: '8px',
             }}
-            type="bare"
+            onClick={toggleIsNegative}
           >
-            <View
-              style={{
-                borderBottomWidth: 1,
-                borderColor: '#e0e0e0',
-                justifyContent: 'center',
-                transform: [{ translateY: 0.5 }],
-                ...style,
-              }}
-            >
-              <Text style={{ fontSize: 15, userSelect: 'none', ...textStyle }}>
-                {amountToCurrency(Math.abs(this.props.value))}
-              </Text>
-            </View>
+            {isNegative ? '-' : '+'}
           </Button>
-        </View>
+        )}
+        <Button
+          onClick={onFocus}
+          // Defines how far touch can start away from the button
+          // hitSlop={{ top: 5, bottom: 5, left: 5, right: 5 }}
+          {...buttonProps}
+          style={{
+            ...(buttonProps && buttonProps.style),
+            ...(focused && { display: 'none' }),
+            ':hover': {
+              backgroundColor: 'transparent',
+            },
+          }}
+          type="bare"
+        >
+          <View
+            style={{
+              borderBottomWidth: 1,
+              borderColor: '#e0e0e0',
+              justifyContent: 'center',
+              transform: [{ translateY: 0.5 }],
+              ...style,
+            }}
+          >
+            <Text style={{ fontSize: 15, userSelect: 'none', ...textStyle }}>
+              {amountToCurrency(Math.abs(value))}
+            </Text>
+          </View>
+        </Button>
       </View>
-    );
-  }
-}
+    </View>
+  );
+});
diff --git a/packages/desktop-client/src/components/mobile/MobileForms.jsx b/packages/desktop-client/src/components/mobile/MobileForms.jsx
index c7f084574ecdd84bfb66b0360584faee1df69416..4a7334fbd8416c0d65eaefc68a19ea1d1412ecd3 100644
--- a/packages/desktop-client/src/components/mobile/MobileForms.jsx
+++ b/packages/desktop-client/src/components/mobile/MobileForms.jsx
@@ -18,7 +18,7 @@ export function FieldLabel({ title, flush, style }) {
         marginTop: flush ? 0 : 25,
         fontSize: 13,
         color: theme.tableRowHeaderText,
-        paddingLeft: styles.mobileEditingPadding,
+        padding: `0 ${styles.mobileEditingPadding}px`,
         textTransform: 'uppercase',
         userSelect: 'none',
         ...style,
@@ -35,7 +35,6 @@ const valueStyle = {
   marginLeft: 8,
   marginRight: 8,
   height: FIELD_HEIGHT,
-  paddingHorizontal: styles.mobileEditingPadding,
 };
 
 export const InputField = forwardRef(function InputField(
@@ -44,7 +43,7 @@ export const InputField = forwardRef(function InputField(
 ) {
   return (
     <Input
-      ref={ref}
+      inputRef={ref}
       autoCorrect="false"
       autoCapitalize="none"
       disabled={disabled}
diff --git a/packages/desktop-client/src/components/modals/EditField.jsx b/packages/desktop-client/src/components/modals/EditField.jsx
index 853c85534be0d036401939dd6dc36c63ba7c8b5b..abd07d79feafaf97a9cc30039792fc2b8ec2e9c7 100644
--- a/packages/desktop-client/src/components/modals/EditField.jsx
+++ b/packages/desktop-client/src/components/modals/EditField.jsx
@@ -34,7 +34,7 @@ function CreatePayeeIcon(props) {
   return <Add {...props} width={14} height={14} />;
 }
 
-export default function EditField({ modalProps, name, onSubmit }) {
+export default function EditField({ modalProps, name, onSubmit, onClose }) {
   const dateFormat = useSelector(
     state => state.prefs.local.dateFormat || 'MM/dd/yyyy',
   );
@@ -44,6 +44,11 @@ export default function EditField({ modalProps, name, onSubmit }) {
 
   const { createPayee } = useActions();
 
+  const onCloseInner = () => {
+    modalProps.onClose();
+    onClose?.();
+  };
+
   function onSelect(value) {
     if (value != null) {
       // Process the value if needed
@@ -53,7 +58,7 @@ export default function EditField({ modalProps, name, onSubmit }) {
 
       onSubmit(name, value);
     }
-    modalProps.onClose();
+    onCloseInner();
   }
 
   const itemStyle = {
@@ -268,6 +273,7 @@ export default function EditField({ modalProps, name, onSubmit }) {
       showHeader={isNarrowWidth}
       focusAfterClose={false}
       {...modalProps}
+      onClose={onCloseInner}
       padding={0}
       style={{
         flex: 0,
diff --git a/packages/desktop-client/src/components/schedules/EditSchedule.jsx b/packages/desktop-client/src/components/schedules/EditSchedule.jsx
index adf731ec447ccc9af94cb5795fdcbe4bde55ee14..5beaa5968c86bd3a5e4fc1835f31cacb753f2ae0 100644
--- a/packages/desktop-client/src/components/schedules/EditSchedule.jsx
+++ b/packages/desktop-client/src/components/schedules/EditSchedule.jsx
@@ -514,8 +514,8 @@ export default function ScheduleDetails({ modalProps, actions, id }) {
           ) : (
             <AmountInput
               id="amount-field"
-              initialValue={state.fields.amount}
-              onChange={value =>
+              value={state.fields.amount}
+              onUpdate={value =>
                 dispatch({
                   type: 'set-field',
                   field: 'amount',
diff --git a/packages/desktop-client/src/components/transactions/MobileTransaction.jsx b/packages/desktop-client/src/components/transactions/MobileTransaction.jsx
index 369ea54da8fab8f866b13ddcbb9e349a60b119d4..7763f1bf639c2ef0097efdd7d56f73d97922df1f 100644
--- a/packages/desktop-client/src/components/transactions/MobileTransaction.jsx
+++ b/packages/desktop-client/src/components/transactions/MobileTransaction.jsx
@@ -1,10 +1,10 @@
 import React, {
-  PureComponent,
-  Component,
   forwardRef,
   useEffect,
   useState,
   useRef,
+  memo,
+  useMemo,
 } from 'react';
 import { useSelector } from 'react-redux';
 import { useParams } from 'react-router-dom';
@@ -21,7 +21,6 @@ import {
   isValid as isValidDate,
 } from 'date-fns';
 import { css } from 'glamor';
-import memoizeOne from 'memoize-one';
 
 import q, { runQuery } from 'loot-core/src/client/query-helpers';
 import { send } from 'loot-core/src/platform/client/fetch';
@@ -32,6 +31,9 @@ import {
   ungroupTransactions,
   updateTransaction,
   realizeTempTransactions,
+  splitTransaction,
+  addSplitTransaction,
+  deleteTransaction,
 } from 'loot-core/src/shared/transactions';
 import {
   titleFirst,
@@ -47,12 +49,17 @@ import { useActions } from '../../hooks/useActions';
 import useCategories from '../../hooks/useCategories';
 import useNavigate from '../../hooks/useNavigate';
 import { useSetThemeColor } from '../../hooks/useSetThemeColor';
-import SvgAdd from '../../icons/v1/Add';
-import SvgTrash from '../../icons/v1/Trash';
+import {
+  SingleActiveEditFormProvider,
+  useSingleActiveEditForm,
+} from '../../hooks/useSingleActiveEditForm';
+import Split from '../../icons/v0/Split';
+import Add from '../../icons/v1/Add';
+import Trash from '../../icons/v1/Trash';
 import ArrowsSynchronize from '../../icons/v2/ArrowsSynchronize';
 import CheckCircle1 from '../../icons/v2/CheckCircle1';
 import Lock from '../../icons/v2/LockClosed';
-import SvgPencilWriteAlternate from '../../icons/v2/PencilWriteAlternate';
+import PencilWriteAlternate from '../../icons/v2/PencilWriteAlternate';
 import { styles, theme } from '../../style';
 import Button from '../common/Button';
 import Text from '../common/Text';
@@ -67,11 +74,13 @@ import {
 } from '../mobile/MobileForms';
 import MobileBackButton from '../MobileBackButton';
 import { Page } from '../Page';
+import { AmountInput } from '../util/AmountInput';
 
 const zIndices = { SECTION_HEADING: 10 };
 
-const getPayeesById = memoizeOne(payees => groupById(payees));
-const getAccountsById = memoizeOne(accounts => groupById(accounts));
+function getFieldName(transactionId, field) {
+  return `${field}-${transactionId}`;
+}
 
 function getDescriptionPretty(transaction, payee, transferAcct) {
   const { amount } = transaction;
@@ -167,80 +176,352 @@ function Status({ status }) {
   );
 }
 
-class TransactionEditInner extends PureComponent {
-  constructor(props) {
-    super(props);
-    this.state = {
-      transactions: props.transactions,
-      editingChild: null,
-    };
-  }
+function Footer({
+  transactions,
+  adding,
+  onAdd,
+  onSave,
+  onSplit,
+  onAddSplit,
+  onEmptySplitFound,
+}) {
+  const [transaction, ...childTransactions] = transactions;
+  const onClickRemainingSplit = () => {
+    if (childTransactions.length === 0) {
+      onSplit(transaction.id);
+    } else {
+      const emptySplitTransaction = childTransactions.find(t => t.amount === 0);
+      if (!emptySplitTransaction) {
+        onAddSplit(transaction.id);
+      } else {
+        onEmptySplitFound?.(emptySplitTransaction.id);
+      }
+    }
+  };
+
+  return (
+    <View
+      style={{
+        paddingLeft: styles.mobileEditingPadding,
+        paddingRight: styles.mobileEditingPadding,
+        paddingTop: 10,
+        paddingBottom: 10,
+        backgroundColor: theme.tableHeaderBackground,
+        borderTopWidth: 1,
+        borderColor: theme.tableBorder,
+      }}
+    >
+      {transaction.error?.type === 'SplitTransactionError' ? (
+        <Button
+          type="primary"
+          style={{ height: 40 }}
+          onClick={onClickRemainingSplit}
+          onPointerDown={e => e.preventDefault()}
+        >
+          <Split width={17} height={17} />
+          <Text
+            style={{
+              ...styles.text,
+              marginLeft: 6,
+            }}
+          >
+            Amount left:{' '}
+            {integerToCurrency(
+              transaction.amount > 0
+                ? transaction.error.difference
+                : -transaction.error.difference,
+            )}
+          </Text>
+        </Button>
+      ) : adding ? (
+        <Button
+          style={{ height: 40 }}
+          onClick={onAdd}
+          onPointerDown={e => e.preventDefault()}
+        >
+          <Add width={17} height={17} style={{ color: theme.formLabelText }} />
+          <Text
+            style={{
+              ...styles.text,
+              color: theme.formLabelText,
+              marginLeft: 5,
+            }}
+          >
+            Add transaction
+          </Text>
+        </Button>
+      ) : (
+        <Button
+          style={{ height: 40 }}
+          onClick={onSave}
+          onPointerDown={e => e.preventDefault()}
+        >
+          <PencilWriteAlternate
+            width={16}
+            height={16}
+            style={{
+              color: theme.formLabelText,
+            }}
+          />
+          <Text
+            style={{
+              ...styles.text,
+              marginLeft: 6,
+              color: theme.formLabelText,
+            }}
+          >
+            Save changes
+          </Text>
+        </Button>
+      )}
+    </View>
+  );
+}
 
-  serializeTransactions = memoizeOne(transactions => {
-    return transactions.map(t =>
-      serializeTransaction(t, this.props.dateFormat),
+const ChildTransactionEdit = forwardRef(
+  (
+    {
+      transaction,
+      amountSign,
+      getCategory,
+      getPrettyPayee,
+      isOffBudget,
+      isBudgetTransfer,
+      onClick,
+      onEdit,
+      onDelete,
+    },
+    ref,
+  ) => {
+    const { editingField, onRequestActiveEdit, onClearActiveEdit } =
+      useSingleActiveEditForm();
+    return (
+      <View
+        innerRef={ref}
+        style={{
+          backgroundColor: theme.tableBackground,
+          borderColor:
+            transaction.amount === 0
+              ? theme.tableBorderSelected
+              : theme.tableBorder,
+          borderWidth: '1px',
+          borderRadius: '5px',
+          padding: '5px',
+          margin: '10px',
+        }}
+      >
+        <View style={{ flexDirection: 'row' }}>
+          <View style={{ flexBasis: '75%' }}>
+            <FieldLabel title="Payee" />
+            <TapField
+              disabled={
+                editingField &&
+                editingField !== getFieldName(transaction.id, 'payee')
+              }
+              value={getPrettyPayee(transaction)}
+              onClick={() => onClick(transaction.id, 'payee')}
+              data-testid={`payee-field-${transaction.id}`}
+            />
+          </View>
+          <View
+            style={{
+              flexBasis: '25%',
+            }}
+          >
+            <FieldLabel title="Amount" style={{ padding: 0 }} />
+            <AmountInput
+              disabled={
+                editingField &&
+                editingField !== getFieldName(transaction.id, 'amount')
+              }
+              focused={transaction.amount === 0}
+              value={amountToInteger(transaction.amount)}
+              zeroSign={amountSign}
+              style={{ marginRight: 8 }}
+              textStyle={{ ...styles.smallText, textAlign: 'right' }}
+              onFocus={() =>
+                onRequestActiveEdit(getFieldName(transaction.id, 'amount'))
+              }
+              onUpdate={value => {
+                const amount = integerToAmount(value);
+                if (transaction.amount !== amount) {
+                  onEdit(transaction, 'amount', amount);
+                } else {
+                  onClearActiveEdit();
+                }
+              }}
+            />
+          </View>
+        </View>
+
+        <View>
+          <FieldLabel title="Category" />
+          <TapField
+            style={{
+              ...((isOffBudget || isBudgetTransfer(transaction)) && {
+                fontStyle: 'italic',
+                color: theme.pageTextSubdued,
+                fontWeight: 300,
+              }),
+            }}
+            value={getCategory(transaction, isOffBudget)}
+            disabled={
+              (editingField &&
+                editingField !== getFieldName(transaction.id, 'category')) ||
+              isOffBudget ||
+              isBudgetTransfer(transaction)
+            }
+            onClick={() => onClick(transaction.id, 'category')}
+            data-testid={`category-field-${transaction.id}`}
+          />
+        </View>
+
+        <View>
+          <FieldLabel title="Notes" />
+          <InputField
+            disabled={
+              editingField &&
+              editingField !== getFieldName(transaction.id, 'notes')
+            }
+            defaultValue={transaction.notes}
+            onFocus={() =>
+              onRequestActiveEdit(getFieldName(transaction.id, 'notes'))
+            }
+            onUpdate={value => onEdit(transaction, 'notes', value)}
+          />
+        </View>
+
+        <View style={{ alignItems: 'center' }}>
+          <Button
+            onClick={() => onDelete(transaction.id)}
+            onPointerDown={e => e.preventDefault()}
+            style={{
+              height: 40,
+              borderWidth: 0,
+              marginLeft: styles.mobileEditingPadding,
+              marginRight: styles.mobileEditingPadding,
+              marginTop: 10,
+              backgroundColor: 'transparent',
+            }}
+            type="bare"
+          >
+            <Trash width={17} height={17} style={{ color: theme.errorText }} />
+            <Text
+              style={{
+                color: theme.errorText,
+                marginLeft: 5,
+                userSelect: 'none',
+              }}
+            >
+              Delete split
+            </Text>
+          </Button>
+        </View>
+      </View>
     );
-  });
+  },
+);
+
+const TransactionEditInner = memo(function TransactionEditInner({
+  adding,
+  accounts,
+  categories,
+  payees,
+  dateFormat,
+  transactions: unserializedTransactions,
+  navigate,
+  pushModal,
+  ...props
+}) {
+  const { editingField, onRequestActiveEdit, onClearActiveEdit } =
+    useSingleActiveEditForm();
+  const [totalAmountFocused, setTotalAmountFocused] = useState(false);
+  const childTransactionElementRefMap = useRef({});
+
+  const payeesById = useMemo(() => groupById(payees), [payees]);
+  const accountsById = useMemo(() => groupById(accounts), [accounts]);
+
+  const getAccount = trans => {
+    return trans?.account && accountsById?.[trans.account];
+  };
 
-  componentDidMount() {
-    if (this.props.adding) {
-      this.amount.focus();
-    }
-  }
+  const getPayee = trans => {
+    return trans?.payee && payeesById?.[trans.payee];
+  };
 
-  componentWillUnmount() {
-    document
-      .querySelector('meta[name="theme-color"]')
-      .setAttribute('content', '#ffffff');
-  }
+  const getTransferAcct = trans => {
+    const payee = trans && getPayee(trans);
+    return payee?.transfer_acct && accountsById?.[payee.transfer_acct];
+  };
+
+  const getPrettyPayee = trans => {
+    const transPayee = trans && getPayee(trans);
+    const transTransferAcct = trans && getTransferAcct(trans);
+    return getDescriptionPretty(trans, transPayee, transTransferAcct);
+  };
+
+  const isBudgetTransfer = trans => {
+    const transferAcct = trans && getTransferAcct(trans);
+    return transferAcct && !transferAcct.offbudget;
+  };
+
+  const getCategory = (trans, isOffBudget) => {
+    return isOffBudget
+      ? 'Off Budget'
+      : isBudgetTransfer(trans)
+      ? 'Transfer'
+      : lookupName(categories, trans.category);
+  };
 
-  openChildEdit = child => {
-    this.setState({ editingChild: child.id });
+  const onTotalAmountEdit = () => {
+    onRequestActiveEdit?.(getFieldName(transaction.id, 'amount'), () => {
+      setTotalAmountFocused(true);
+      return () => setTotalAmountFocused(false);
+    });
   };
 
-  onAdd = () => {
-    this.onSave();
+  useEffect(() => {
+    if (adding) {
+      onTotalAmountEdit();
+    }
+  }, []);
+
+  const onTotalAmountUpdate = value => {
+    if (transaction.amount !== value) {
+      onEdit(transaction, 'amount', value.toString());
+    } else {
+      onClearActiveEdit();
+    }
   };
 
-  onSave = async () => {
+  const onSave = async () => {
+    const [transaction] = unserializedTransactions;
+
     const onConfirmSave = async () => {
-      let { transactions } = this.state;
-      const [transaction, ..._childTransactions] = transactions;
       const { account: accountId } = transaction;
-      const account = getAccountsById(this.props.accounts)[accountId];
+      const account = accountsById[accountId];
 
-      if (transactions.find(t => t.account == null)) {
+      if (unserializedTransactions.find(t => t.account == null)) {
         // Ignore transactions if any of them don't have an account
         // TODO: Should we display validation error?
         return;
       }
 
-      // Since we don't own the state, we have to handle the case where
-      // the user saves while editing an input. We won't have the
-      // updated value so we "apply" a queued change. Maybe there's a
-      // better way to do this (lift the state?)
-      if (this._queuedChange) {
-        const [transaction, name, value] = this._queuedChange;
-        transactions = await this.onEdit(transaction, name, value);
+      let transactionsToSave = unserializedTransactions;
+      if (adding) {
+        transactionsToSave = realizeTempTransactions(unserializedTransactions);
       }
 
-      if (this.props.adding) {
-        transactions = realizeTempTransactions(transactions);
-      }
-
-      this.props.onSave(transactions);
-      this.props.navigate(`/accounts/${account.id}`, { replace: true });
+      props.onSave(transactionsToSave);
+      navigate(`/accounts/${account.id}`, { replace: true });
     };
 
-    const { transactions } = this.state;
-    const [transaction] = transactions;
-
     if (transaction.reconciled) {
       // On mobile any save gives the warning.
       // On the web only certain changes trigger a warning.
       // Should we bring that here as well? Or does the nature of the editing form
       // make this more appropriate?
-      this.props.pushModal('confirm-transaction-edit', {
+      pushModal('confirm-transaction-edit', {
         onConfirm: onConfirmSave,
         confirmReason: 'editReconciled',
       });
@@ -249,72 +530,58 @@ class TransactionEditInner extends PureComponent {
     }
   };
 
-  onSaveChild = childTransaction => {
-    this.setState({ editingChild: null });
+  const onAdd = () => {
+    onSave();
   };
 
-  onEdit = async (transaction, name, value) => {
-    const { transactions } = this.state;
-
-    let newTransaction = { ...transaction, [name]: value };
-    if (this.props.onEdit) {
-      newTransaction = await this.props.onEdit(newTransaction);
-    }
-
-    const { data: newTransactions } = updateTransaction(
-      transactions,
-      deserializeTransaction(newTransaction, null, this.props.dateFormat),
-    );
-
-    this._queuedChange = null;
-    this.setState({ transactions: newTransactions });
-    return newTransactions;
-  };
-
-  onQueueChange = (transaction, name, value) => {
-    // This is an ugly hack to solve the problem that input's blur
-    // events are not fired when unmounting. If the user has focused
-    // an input and swipes back, it should still save, but because the
-    // blur event is not fired we need to manually track the latest
-    // change and apply it ourselves when unmounting
-    this._queuedChange = [transaction, name, value];
+  const onEdit = async (transaction, name, value) => {
+    const newTransaction = { ...transaction, [name]: value };
+    await props.onEdit(newTransaction);
+    onClearActiveEdit();
   };
 
-  onClick = (transactionId, name) => {
-    const { dateFormat } = this.props;
-
-    this.props.pushModal('edit-field', {
-      name,
-      onSubmit: (name, value) => {
-        const { transactions } = this.state;
-        const transaction = transactions.find(t => t.id === transactionId);
-        // This is a deficiency of this API, need to fix. It
-        // assumes that it receives a serialized transaction,
-        // but we only have access to the raw transaction
-        this.onEdit(serializeTransaction(transaction, dateFormat), name, value);
-      },
+  const onClick = (transactionId, name) => {
+    onRequestActiveEdit?.(getFieldName(transaction.id, 'payee'), () => {
+      pushModal('edit-field', {
+        name,
+        onSubmit: (name, value) => {
+          const transaction = unserializedTransactions.find(
+            t => t.id === transactionId,
+          );
+          // This is a deficiency of this API, need to fix. It
+          // assumes that it receives a serialized transaction,
+          // but we only have access to the raw transaction
+          onEdit(serializeTransaction(transaction, dateFormat), name, value);
+        },
+        onClose: () => {
+          onClearActiveEdit();
+        },
+      });
     });
   };
 
-  onDelete = () => {
+  const onDelete = id => {
+    const [transaction, ..._childTransactions] = unserializedTransactions;
+
     const onConfirmDelete = () => {
-      this.props.onDelete();
+      props.onDelete(id);
+
+      if (transaction.id !== id) {
+        // Only a child transaction was deleted.
+        onClearActiveEdit();
+        return;
+      }
 
-      const { transactions } = this.state;
-      const [transaction, ..._childTransactions] = transactions;
       const { account: accountId } = transaction;
       if (accountId) {
-        this.props.navigate(`/accounts/${accountId}`, { replace: true });
+        navigate(`/accounts/${accountId}`, { replace: true });
       } else {
-        this.props.navigate(-1);
+        navigate(-1);
       }
     };
 
-    const { transactions } = this.state;
-    const [transaction] = transactions;
-
     if (transaction.reconciled) {
-      this.props.pushModal('confirm-transaction-edit', {
+      pushModal('confirm-transaction-edit', {
         onConfirm: onConfirmDelete,
         confirmReason: 'deleteReconciled',
       });
@@ -323,316 +590,322 @@ class TransactionEditInner extends PureComponent {
     }
   };
 
-  render() {
-    const { adding, categories, accounts, payees } = this.props;
-    const transactions = this.serializeTransactions(
-      this.state.transactions || [],
-    );
-    const [transaction, ..._childTransactions] = transactions;
-    const { payee: payeeId, category, account: accountId } = transaction;
-
-    // Child transactions should always default to the signage
-    // of the parent transaction
-    // const forcedSign = transaction.amount < 0 ? 'negative' : 'positive';
-
-    const account = getAccountsById(accounts)[accountId];
-    const isOffBudget = account && !!account.offbudget;
-    const payee = payees && payeeId && getPayeesById(payees)[payeeId];
-    const transferAcct =
-      payee &&
-      payee.transfer_acct &&
-      getAccountsById(accounts)[payee.transfer_acct];
-    const isBudgetTransfer = transferAcct && !transferAcct.offbudget;
-    const descriptionPretty = getDescriptionPretty(
-      transaction,
-      payee,
-      transferAcct,
-    );
+  const scrollChildTransactionIntoView = id => {
+    const childTransactionEditElement =
+      childTransactionElementRefMap.current?.[id];
+    childTransactionEditElement?.scrollIntoView({
+      behavior: 'smooth',
+    });
+  };
 
-    const transactionDate = parseDate(
-      transaction.date,
-      this.props.dateFormat,
-      new Date(),
-    );
-    const dateDefaultValue = monthUtils.dayFromDate(transactionDate);
+  const onAddSplit = id => {
+    props.onAddSplit(id);
+  };
 
-    return (
-      <Page
-        title={
-          payeeId == null
-            ? adding
-              ? 'New Transaction'
-              : 'Transaction'
-            : descriptionPretty
-        }
-        titleStyle={{
-          fontSize: 16,
-          fontWeight: 500,
-        }}
-        style={{
-          flex: 1,
-          backgroundColor: theme.mobilePageBackground,
-        }}
-        headerLeftContent={<MobileBackButton />}
-        footer={
-          <View
-            style={{
-              paddingLeft: styles.mobileEditingPadding,
-              paddingRight: styles.mobileEditingPadding,
-              paddingTop: 10,
-              paddingBottom: 10,
-              backgroundColor: theme.tableHeaderBackground,
-              borderTopWidth: 1,
-              borderColor: theme.tableBorder,
-            }}
-          >
-            {adding ? (
-              <Button style={{ height: 40 }} onClick={() => this.onAdd()}>
-                <SvgAdd
-                  width={17}
-                  height={17}
-                  style={{ color: theme.formLabelText }}
-                />
-                <Text
-                  style={{
-                    ...styles.text,
-                    color: theme.formLabelText,
-                    marginLeft: 5,
-                  }}
-                >
-                  Add transaction
-                </Text>
-              </Button>
-            ) : (
-              <Button style={{ height: 40 }} onClick={() => this.onSave()}>
-                <SvgPencilWriteAlternate
-                  style={{
-                    width: 16,
-                    height: 16,
-                    color: theme.formInputText,
-                  }}
-                />
-                <Text
-                  style={{
-                    ...styles.text,
-                    marginLeft: 6,
-                    color: theme.formInputText,
-                  }}
-                >
-                  Save changes
-                </Text>
-              </Button>
-            )}
-          </View>
-        }
-        padding={0}
-      >
-        <View style={{ flexShrink: 0, marginTop: 20, marginBottom: 20 }}>
-          <View
-            style={{
-              alignItems: 'center',
+  const onSplit = id => {
+    props.onSplit(id);
+  };
+
+  const onEmptySplitFound = id => {
+    scrollChildTransactionIntoView(id);
+  };
+
+  const transactions = useMemo(
+    () =>
+      unserializedTransactions.map(t => serializeTransaction(t, dateFormat)) ||
+      [],
+    [unserializedTransactions, dateFormat],
+  );
+
+  const [transaction, ...childTransactions] = transactions;
+
+  useEffect(() => {
+    const noAmountTransaction = childTransactions.find(t => t.amount === 0);
+    if (noAmountTransaction) {
+      scrollChildTransactionIntoView(noAmountTransaction.id);
+    }
+  }, [childTransactions]);
+
+  // Child transactions should always default to the signage
+  // of the parent transaction
+  const childAmountSign = transaction.amount <= 0 ? '-' : '+';
+
+  const account = getAccount(transaction);
+  const isOffBudget = account && !!account.offbudget;
+  const title = getDescriptionPretty(
+    transaction,
+    getPayee(transaction),
+    getTransferAcct(transaction),
+  );
+
+  const transactionDate = parseDate(transaction.date, dateFormat, new Date());
+  const dateDefaultValue = monthUtils.dayFromDate(transactionDate);
+
+  return (
+    <Page
+      title={
+        transaction.payee == null
+          ? adding
+            ? 'New Transaction'
+            : 'Transaction'
+          : title
+      }
+      titleStyle={{
+        fontSize: 16,
+        fontWeight: 500,
+      }}
+      style={{
+        flex: 1,
+        backgroundColor: theme.mobilePageBackground,
+      }}
+      headerLeftContent={<MobileBackButton />}
+      footer={
+        <Footer
+          transactions={transactions}
+          adding={adding}
+          onAdd={onAdd}
+          onSave={onSave}
+          onSplit={onSplit}
+          onAddSplit={onAddSplit}
+          onEmptySplitFound={onEmptySplitFound}
+        />
+      }
+      padding={0}
+    >
+      <View style={{ flexShrink: 0, marginTop: 20, marginBottom: 20 }}>
+        <View
+          style={{
+            alignItems: 'center',
+          }}
+        >
+          <FieldLabel title="Amount" flush style={{ marginBottom: 0 }} />
+          <FocusableAmountInput
+            value={transaction.amount}
+            zeroSign="-"
+            focused={totalAmountFocused}
+            onFocus={onTotalAmountEdit}
+            onUpdate={onTotalAmountUpdate}
+            focusedStyle={{
+              width: 'auto',
+              padding: '5px',
+              paddingLeft: '20px',
+              paddingRight: '20px',
+              minWidth: 120,
+              transform: [{ translateY: -0.5 }],
             }}
-          >
-            <FieldLabel
-              title="Amount"
-              flush
-              style={{ marginBottom: 0, paddingLeft: 0 }}
-            />
-            <FocusableAmountInput
-              ref={el => (this.amount = el)}
-              value={transaction.amount}
-              zeroIsNegative={true}
-              onBlur={value =>
-                this.onEdit(transaction, 'amount', value.toString())
-              }
-              onChange={value =>
-                this.onQueueChange(transaction, 'amount', value)
-              }
-              style={{ transform: [] }}
-              focusedStyle={{
-                width: 'auto',
-                padding: '5px',
-                paddingLeft: '20px',
-                paddingRight: '20px',
-                minWidth: 120,
-                transform: [{ translateY: -0.5 }],
-              }}
-              textStyle={{ fontSize: 30, textAlign: 'center' }}
-            />
-          </View>
+            textStyle={{ fontSize: 30, textAlign: 'center' }}
+          />
+        </View>
+
+        <View>
+          <FieldLabel title="Payee" />
+          <TapField
+            disabled={
+              editingField &&
+              editingField !== getFieldName(transaction.id, 'payee')
+            }
+            value={getPrettyPayee(transaction)}
+            onClick={() => onClick(transaction.id, 'payee')}
+            data-testid="payee-field"
+          />
+        </View>
 
+        {!transaction.is_parent && (
           <View>
-            <FieldLabel title="Payee" />
+            <FieldLabel title="Category" />
             <TapField
-              value={descriptionPretty}
-              onClick={() => this.onClick(transaction.id, 'payee')}
-              data-testid="payee-field"
+              style={{
+                ...((isOffBudget || isBudgetTransfer(transaction)) && {
+                  fontStyle: 'italic',
+                  color: theme.pageTextSubdued,
+                  fontWeight: 300,
+                }),
+              }}
+              value={getCategory(transaction, isOffBudget)}
+              disabled={
+                (editingField &&
+                  editingField !== getFieldName(transaction.id, 'category')) ||
+                isOffBudget ||
+                isBudgetTransfer(transaction)
+              }
+              onClick={() => onClick(transaction.id, 'category')}
+              data-testid="category-field"
             />
           </View>
+        )}
+
+        {childTransactions.map(childTrans => (
+          <ChildTransactionEdit
+            key={childTrans.id}
+            transaction={childTrans}
+            amountSign={childAmountSign}
+            ref={r => {
+              childTransactionElementRefMap.current = {
+                ...childTransactionElementRefMap.current,
+                [childTrans.id]: r,
+              };
+            }}
+            isOffBudget={isOffBudget}
+            getCategory={getCategory}
+            getPrettyPayee={getPrettyPayee}
+            isBudgetTransfer={isBudgetTransfer}
+            onEdit={onEdit}
+            onClick={onClick}
+            onDelete={onDelete}
+          />
+        ))}
 
-          <View>
-            <FieldLabel
-              title={transaction.is_parent ? 'Categories (split)' : 'Category'}
-            />
-            {!transaction.is_parent ? (
-              <TapField
+        {transaction.amount !== 0 && childTransactions.length === 0 && (
+          <View style={{ alignItems: 'center' }}>
+            <Button
+              disabled={editingField}
+              style={{
+                height: 40,
+                borderWidth: 0,
+                marginLeft: styles.mobileEditingPadding,
+                marginRight: styles.mobileEditingPadding,
+                marginTop: 10,
+                backgroundColor: 'transparent',
+              }}
+              onClick={() => onSplit(transaction.id)}
+              type="bare"
+            >
+              <Split
+                width={17}
+                height={17}
+                style={{ color: theme.formLabelText }}
+              />
+              <Text
                 style={{
-                  ...((isBudgetTransfer || isOffBudget) && {
-                    fontStyle: 'italic',
-                    color: theme.pageTextSubdued,
-                    fontWeight: 300,
-                  }),
+                  marginLeft: 5,
+                  userSelect: 'none',
+                  color: theme.formLabelText,
                 }}
-                value={
-                  isOffBudget
-                    ? 'Off Budget'
-                    : isBudgetTransfer
-                    ? 'Transfer'
-                    : lookupName(categories, category)
-                }
-                disabled={isBudgetTransfer || isOffBudget}
-                // TODO: the button to turn this transaction into a split
-                // transaction was on top of the category button in the native
-                // app, on the right-hand side
-                //
-                // On the web this doesn't work well and react gets upset if
-                // nest a button in a button.
-                //
-                // rightContent={
-                //   <Button
-                //     contentStyle={{
-                //       paddingVertical: 4,
-                //       paddingHorizontal: 15,
-                //       margin: 0,
-                //     }}
-                //     onPress={this.onSplit}
-                //   >
-                //     Split
-                //   </Button>
-                // }
-                onClick={() => this.onClick(transaction.id, 'category')}
-                data-testid="category-field"
-              />
-            ) : (
-              <Text style={{ paddingLeft: styles.mobileEditingPadding }}>
-                Split transaction editing is not supported on mobile at this
-                time.
+              >
+                Split
               </Text>
-            )}
-          </View>
-
-          <View>
-            <FieldLabel title="Account" />
-            <TapField
-              disabled={!adding}
-              value={account ? account.name : null}
-              onClick={() => this.onClick(transaction.id, 'account')}
-              data-testid="account-field"
-            />
-          </View>
-
-          <View style={{ flexDirection: 'row' }}>
-            <View style={{ flex: 1 }}>
-              <FieldLabel title="Date" />
-              <InputField
-                type="date"
-                required
-                style={{ color: theme.tableText, minWidth: '150px' }}
-                defaultValue={dateDefaultValue}
-                onUpdate={value =>
-                  this.onEdit(
-                    transaction,
-                    'date',
-                    formatDate(parseISO(value), this.props.dateFormat),
-                  )
-                }
-                onChange={e =>
-                  this.onQueueChange(
-                    transaction,
-                    'date',
-                    formatDate(parseISO(e.target.value), this.props.dateFormat),
-                  )
-                }
-              />
-            </View>
-            {transaction.reconciled ? (
-              <View style={{ marginLeft: 0, marginRight: 8 }}>
-                <FieldLabel title="Reconciled" />
-                <BooleanField
-                  checked
-                  style={{
-                    margin: 'auto',
-                    width: 22,
-                    height: 22,
-                  }}
-                  disabled
-                />
-              </View>
-            ) : (
-              <View style={{ marginLeft: 0, marginRight: 8 }}>
-                <FieldLabel title="Cleared" />
-                <BooleanField
-                  checked={transaction.cleared}
-                  onUpdate={checked =>
-                    this.onEdit(transaction, 'cleared', checked)
-                  }
-                  style={{
-                    margin: 'auto',
-                    width: 22,
-                    height: 22,
-                  }}
-                />
-              </View>
-            )}
+            </Button>
           </View>
+        )}
+
+        <View>
+          <FieldLabel title="Account" />
+          <TapField
+            disabled={
+              !adding ||
+              (editingField &&
+                editingField !== getFieldName(transaction.id, 'account'))
+            }
+            value={account?.name}
+            onClick={() => onClick(transaction.id, 'account')}
+            data-testid="account-field"
+          />
+        </View>
 
-          <View>
-            <FieldLabel title="Notes" />
+        <View style={{ flexDirection: 'row' }}>
+          <View style={{ flex: 1 }}>
+            <FieldLabel title="Date" />
             <InputField
-              defaultValue={transaction.notes}
-              onUpdate={value => this.onEdit(transaction, 'notes', value)}
-              onChange={e =>
-                this.onQueueChange(transaction, 'notes', e.target.value)
+              type="date"
+              disabled={
+                editingField &&
+                editingField !== getFieldName(transaction.id, 'date')
+              }
+              required
+              style={{ color: theme.tableText, minWidth: '150px' }}
+              defaultValue={dateDefaultValue}
+              onFocus={() =>
+                onRequestActiveEdit(getFieldName(transaction.id, 'date'))
+              }
+              onUpdate={value =>
+                onEdit(
+                  transaction,
+                  'date',
+                  formatDate(parseISO(value), dateFormat),
+                )
               }
             />
           </View>
-
-          {!adding && (
-            <View style={{ alignItems: 'center' }}>
-              <Button
-                onClick={() => this.onDelete()}
+          {transaction.reconciled ? (
+            <View style={{ marginLeft: 0, marginRight: 8 }}>
+              <FieldLabel title="Reconciled" />
+              <BooleanField
+                disabled
+                checked
                 style={{
-                  height: 40,
-                  borderWidth: 0,
-                  marginLeft: styles.mobileEditingPadding,
-                  marginRight: styles.mobileEditingPadding,
-                  marginTop: 10,
-                  backgroundColor: 'transparent',
+                  margin: 'auto',
+                  width: 22,
+                  height: 22,
                 }}
-                type="bare"
-              >
-                <SvgTrash
-                  width={17}
-                  height={17}
-                  style={{ color: theme.errorText }}
-                />
-                <Text
-                  style={{
-                    color: theme.errorText,
-                    marginLeft: 5,
-                    userSelect: 'none',
-                  }}
-                >
-                  Delete transaction
-                </Text>
-              </Button>
+              />
+            </View>
+          ) : (
+            <View style={{ marginLeft: 0, marginRight: 8 }}>
+              <FieldLabel title="Cleared" />
+              <BooleanField
+                disabled={editingField}
+                checked={transaction.cleared}
+                onUpdate={checked => onEdit(transaction, 'cleared', checked)}
+                style={{
+                  margin: 'auto',
+                  width: 22,
+                  height: 22,
+                }}
+              />
             </View>
           )}
         </View>
-      </Page>
-    );
-  }
-}
+
+        <View>
+          <FieldLabel title="Notes" />
+          <InputField
+            disabled={
+              editingField &&
+              editingField !== getFieldName(transaction.id, 'notes')
+            }
+            defaultValue={transaction.notes}
+            onFocus={() => {
+              onRequestActiveEdit(getFieldName(transaction.id, 'notes'));
+            }}
+            onUpdate={value => onEdit(transaction, 'notes', value)}
+          />
+        </View>
+
+        {!adding && (
+          <View style={{ alignItems: 'center' }}>
+            <Button
+              onClick={() => onDelete(transaction.id)}
+              style={{
+                height: 40,
+                borderWidth: 0,
+                marginLeft: styles.mobileEditingPadding,
+                marginRight: styles.mobileEditingPadding,
+                marginTop: 10,
+                backgroundColor: 'transparent',
+              }}
+              type="bare"
+            >
+              <Trash
+                width={17}
+                height={17}
+                style={{ color: theme.errorText }}
+              />
+              <Text
+                style={{
+                  color: theme.errorText,
+                  marginLeft: 5,
+                  userSelect: 'none',
+                }}
+              >
+                Delete transaction
+              </Text>
+            </Button>
+          </View>
+        )}
+      </View>
+    </Page>
+  );
+});
 
 function isTemporary(transaction) {
   return transaction.id.indexOf('temp') === 0;
@@ -654,10 +927,10 @@ function TransactionEditUnconnected(props) {
   const { categories, accounts, payees, lastTransaction, dateFormat } = props;
   const { id: accountId, transactionId } = useParams();
   const navigate = useNavigate();
-  const [fetchedTransactions, setFetchedTransactions] = useState(null);
-  let transactions = [];
-  let adding = false;
-  let deleted = false;
+  const [transactions, setTransactions] = useState([]);
+  const [fetchedTransactions, setFetchedTransactions] = useState([]);
+  const adding = useRef(false);
+  const deleted = useRef(false);
   useSetThemeColor(theme.mobileViewTheme);
 
   useEffect(() => {
@@ -667,56 +940,62 @@ function TransactionEditUnconnected(props) {
     props.getPayees();
 
     async function fetchTransaction() {
-      let transactions = [];
-      if (transactionId) {
-        // Query for the transaction based on the ID with grouped splits.
-        //
-        // This means if the transaction in question is a split transaction, its
-        // subtransactions will be returned in the `substransactions` property on
-        // the parent transaction.
-        //
-        // The edit item components expect to work with a flat array of
-        // transactions when handling splits, so we call ungroupTransactions to
-        // flatten parent and children into one array.
-        const { data } = await runQuery(
-          q('transactions')
-            .filter({ id: transactionId })
-            .select('*')
-            .options({ splits: 'grouped' }),
-        );
-        transactions = ungroupTransactions(data);
-        setFetchedTransactions(transactions);
-      }
+      // Query for the transaction based on the ID with grouped splits.
+      //
+      // This means if the transaction in question is a split transaction, its
+      // subtransactions will be returned in the `substransactions` property on
+      // the parent transaction.
+      //
+      // The edit item components expect to work with a flat array of
+      // transactions when handling splits, so we call ungroupTransactions to
+      // flatten parent and children into one array.
+      const { data } = await runQuery(
+        q('transactions')
+          .filter({ id: transactionId })
+          .select('*')
+          .options({ splits: 'grouped' }),
+      );
+      setFetchedTransactions(ungroupTransactions(data));
+    }
+    if (transactionId) {
+      fetchTransaction();
+    } else {
+      adding.current = true;
     }
-    fetchTransaction();
   }, [transactionId]);
 
+  useEffect(() => {
+    setTransactions(fetchedTransactions);
+  }, [fetchedTransactions]);
+
+  useEffect(() => {
+    if (adding.current) {
+      setTransactions(
+        makeTemporaryTransactions(
+          accountId || lastTransaction?.account || null,
+          lastTransaction?.date,
+        ),
+      );
+    }
+  }, [adding.current, accountId, lastTransaction]);
+
   if (
     categories.length === 0 ||
     accounts.length === 0 ||
-    (transactionId && !fetchedTransactions)
+    transactions.length === 0
   ) {
     return null;
   }
 
-  if (!transactionId) {
-    transactions = makeTemporaryTransactions(
-      accountId || (lastTransaction && lastTransaction.account) || null,
-      lastTransaction && lastTransaction.date,
-    );
-    adding = true;
-  } else {
-    transactions = fetchedTransactions;
-  }
-
   const onEdit = async transaction => {
+    let newTransaction = transaction;
     // Run the rules to auto-fill in any data. Right now we only do
     // this on new transactions because that's how desktop works.
     if (isTemporary(transaction)) {
       const afterRules = await send('rules-run', { transaction });
       const diff = getChangedValues(transaction, afterRules);
 
-      const newTransaction = { ...transaction };
+      newTransaction = { ...transaction };
       if (diff) {
         Object.keys(diff).forEach(field => {
           if (newTransaction[field] == null) {
@@ -724,18 +1003,21 @@ function TransactionEditUnconnected(props) {
           }
         });
       }
-      return newTransaction;
     }
 
-    return transaction;
+    const { data: newTransactions } = updateTransaction(
+      transactions,
+      deserializeTransaction(newTransaction, null, dateFormat),
+    );
+    setTransactions(newTransactions);
   };
 
   const onSave = async newTransactions => {
-    if (deleted) {
+    if (deleted.current) {
       return;
     }
 
-    const changes = diffItems(transactions || [], newTransactions);
+    const changes = diffItems(fetchedTransactions || [], newTransactions);
     if (
       changes.added.length > 0 ||
       changes.updated.length > 0 ||
@@ -755,24 +1037,40 @@ function TransactionEditUnconnected(props) {
       // }
     }
 
-    if (adding) {
+    if (adding.current) {
       // The first one is always the "parent" and the only one we care
       // about
       props.setLastTransaction(newTransactions[0]);
     }
   };
 
-  const onDelete = async () => {
-    if (adding) {
+  const onDelete = async id => {
+    const changes = deleteTransaction(transactions, id);
+
+    if (adding.current) {
       // Adding a new transactions, this disables saving when the component unmounts
-      deleted = true;
+      deleted.current = true;
     } else {
-      const changes = { deleted: transactions };
-      const _remoteUpdates = await send('transactions-batch-update', changes);
+      const _remoteUpdates = await send('transactions-batch-update', {
+        deleted: changes.diff.deleted,
+      });
+
       // if (onTransactionsChange) {
       //   onTransactionsChange({ ...changes, updated: remoteUpdates });
       // }
     }
+
+    setTransactions(changes.data);
+  };
+
+  const onAddSplit = id => {
+    const changes = addSplitTransaction(transactions, id);
+    setTransactions(changes.data);
+  };
+
+  const onSplit = id => {
+    const changes = splitTransaction(transactions, id);
+    setTransactions(changes.data);
   };
 
   return (
@@ -784,21 +1082,18 @@ function TransactionEditUnconnected(props) {
     >
       <TransactionEditInner
         transactions={transactions}
-        adding={adding}
+        adding={adding.current}
         categories={categories}
         accounts={accounts}
         payees={payees}
         pushModal={props.pushModal}
         navigate={navigate}
-        // TODO: ChildEdit is complicated and heavily relies on RN
-        // renderChildEdit={props => <ChildEdit {...props} />}
-        renderChildEdit={props => {}}
         dateFormat={dateFormat}
-        // TODO: was this a mistake in the original code?
-        // onTapField={this.onTapField}
         onEdit={onEdit}
         onSave={onSave}
         onDelete={onDelete}
+        onSplit={onSplit}
+        onAddSplit={onAddSplit}
       />
     </View>
   );
@@ -815,188 +1110,198 @@ export const TransactionEdit = props => {
   const actions = useActions();
 
   return (
-    <TransactionEditUnconnected
-      {...props}
-      {...actions}
-      categories={categories}
-      payees={payees}
-      lastTransaction={lastTransaction}
-      accounts={accounts}
-      dateFormat={dateFormat}
-    />
+    <SingleActiveEditFormProvider formName="mobile-transaction">
+      <TransactionEditUnconnected
+        {...props}
+        {...actions}
+        categories={categories}
+        payees={payees}
+        lastTransaction={lastTransaction}
+        accounts={accounts}
+        dateFormat={dateFormat}
+      />
+    </SingleActiveEditFormProvider>
   );
 };
 
-class Transaction extends PureComponent {
-  render() {
-    const {
-      transaction,
-      accounts,
-      categories,
-      payees,
-      showCategory,
-      added,
-      onSelect,
-      style,
-    } = this.props;
-    const {
-      id,
-      payee: payeeId,
-      amount: originalAmount,
-      category,
-      cleared,
-      is_parent,
-      notes,
-      schedule,
-    } = transaction;
-
-    let amount = originalAmount;
-    if (isPreviewId(id)) {
-      amount = getScheduledAmount(amount);
-    }
+const Transaction = memo(function Transaction({
+  transaction,
+  accounts,
+  categories,
+  payees,
+  showCategory,
+  added,
+  onSelect,
+  style,
+}) {
+  const accountsById = useMemo(() => groupById(accounts), [accounts]);
+  const payeesById = useMemo(() => groupById(payees), [payees]);
+
+  const {
+    id,
+    payee: payeeId,
+    amount: originalAmount,
+    category: categoryId,
+    cleared,
+    is_parent: isParent,
+    notes,
+    schedule,
+  } = transaction;
+
+  let amount = originalAmount;
+  if (isPreviewId(id)) {
+    amount = getScheduledAmount(amount);
+  }
 
-    const categoryName = lookupName(categories, category);
+  const categoryName = lookupName(categories, categoryId);
 
-    const payee = payees && payeeId && getPayeesById(payees)[payeeId];
-    const transferAcct =
-      payee &&
-      payee.transfer_acct &&
-      getAccountsById(accounts)[payee.transfer_acct];
+  const payee = payeesById && payeeId && payeesById[payeeId];
+  const transferAcct =
+    payee && payee.transfer_acct && accountsById[payee.transfer_acct];
 
-    const prettyDescription = getDescriptionPretty(
-      transaction,
-      payee,
-      transferAcct,
-    );
-    const prettyCategory = transferAcct
-      ? 'Transfer'
-      : is_parent
-      ? 'Split'
-      : categoryName;
-
-    const isPreview = isPreviewId(id);
-    const isReconciled = transaction.reconciled;
-    const textStyle = isPreview && {
-      fontStyle: 'italic',
-      color: theme.pageTextLight,
-    };
+  const prettyDescription = getDescriptionPretty(
+    transaction,
+    payee,
+    transferAcct,
+  );
+  const prettyCategory = transferAcct
+    ? 'Transfer'
+    : isParent
+    ? 'Split'
+    : categoryName;
+
+  const isPreview = isPreviewId(id);
+  const isReconciled = transaction.reconciled;
+  const textStyle = isPreview && {
+    fontStyle: 'italic',
+    color: theme.pageTextLight,
+  };
 
-    return (
-      <Button
-        onClick={() => onSelect(transaction)}
+  return (
+    <Button
+      onClick={() => onSelect(transaction)}
+      style={{
+        backgroundColor: theme.tableBackground,
+        border: 'none',
+        width: '100%',
+      }}
+    >
+      <ListItem
         style={{
-          backgroundColor: theme.tableBackground,
-          border: 'none',
-          width: '100%',
+          flex: 1,
+          height: 60,
+          padding: '5px 10px', // remove padding when Button is back
+          ...(isPreview && {
+            backgroundColor: theme.tableRowHeaderBackground,
+          }),
+          ...style,
         }}
       >
-        <ListItem
-          style={{
-            flex: 1,
-            height: 60,
-            padding: '5px 10px', // remove padding when Button is back
-            ...(isPreview && {
-              backgroundColor: theme.tableRowHeaderBackground,
-            }),
-            ...style,
-          }}
-        >
-          <View style={{ flex: 1 }}>
-            <View style={{ flexDirection: 'row', alignItems: 'center' }}>
-              {schedule && (
-                <ArrowsSynchronize
+        <View style={{ flex: 1 }}>
+          <View style={{ flexDirection: 'row', alignItems: 'center' }}>
+            {schedule && (
+              <ArrowsSynchronize
+                style={{
+                  width: 12,
+                  height: 12,
+                  marginRight: 5,
+                  color: textStyle.color || theme.menuItemText,
+                }}
+              />
+            )}
+            <TextOneLine
+              style={{
+                ...styles.text,
+                ...textStyle,
+                fontSize: 14,
+                fontWeight: added ? '600' : '400',
+                ...(prettyDescription === '' && {
+                  color: theme.tableTextLight,
+                  fontStyle: 'italic',
+                }),
+              }}
+            >
+              {prettyDescription || 'Empty'}
+            </TextOneLine>
+          </View>
+          {isPreview ? (
+            <Status status={notes} />
+          ) : (
+            <View
+              style={{
+                flexDirection: 'row',
+                alignItems: 'center',
+                marginTop: 3,
+              }}
+            >
+              {isReconciled ? (
+                <Lock
+                  style={{
+                    width: 11,
+                    height: 11,
+                    color: theme.noticeTextLight,
+                    marginRight: 5,
+                  }}
+                />
+              ) : (
+                <CheckCircle1
                   style={{
-                    width: 12,
-                    height: 12,
+                    width: 11,
+                    height: 11,
+                    color: cleared
+                      ? theme.noticeTextLight
+                      : theme.pageTextSubdued,
                     marginRight: 5,
-                    color: textStyle.color || theme.menuItemText,
                   }}
                 />
               )}
-              <TextOneLine
-                style={{
-                  ...styles.text,
-                  ...textStyle,
-                  fontSize: 14,
-                  fontWeight: added ? '600' : '400',
-                  ...(prettyDescription === '' && {
-                    color: theme.tableTextLight,
-                    fontStyle: 'italic',
-                  }),
-                }}
-              >
-                {prettyDescription || 'Empty'}
-              </TextOneLine>
+              {showCategory && (
+                <TextOneLine
+                  style={{
+                    fontSize: 11,
+                    marginTop: 1,
+                    fontWeight: '400',
+                    color: prettyCategory
+                      ? theme.tableTextSelected
+                      : theme.menuItemTextSelected,
+                    fontStyle: prettyCategory ? null : 'italic',
+                    textAlign: 'left',
+                  }}
+                >
+                  {prettyCategory || 'Uncategorized'}
+                </TextOneLine>
+              )}
             </View>
-            {isPreview ? (
-              <Status status={notes} />
-            ) : (
-              <View
-                style={{
-                  flexDirection: 'row',
-                  alignItems: 'center',
-                  marginTop: 3,
-                }}
-              >
-                {isReconciled ? (
-                  <Lock
-                    style={{
-                      width: 11,
-                      height: 11,
-                      color: theme.noticeTextLight,
-                      marginRight: 5,
-                    }}
-                  />
-                ) : (
-                  <CheckCircle1
-                    style={{
-                      width: 11,
-                      height: 11,
-                      color: cleared
-                        ? theme.noticeTextLight
-                        : theme.pageTextSubdued,
-                      marginRight: 5,
-                    }}
-                  />
-                )}
-                {showCategory && (
-                  <TextOneLine
-                    style={{
-                      fontSize: 11,
-                      marginTop: 1,
-                      fontWeight: '400',
-                      color: prettyCategory
-                        ? theme.tableTextSelected
-                        : theme.menuItemTextSelected,
-                      fontStyle: prettyCategory ? null : 'italic',
-                      textAlign: 'left',
-                    }}
-                  >
-                    {prettyCategory || 'Uncategorized'}
-                  </TextOneLine>
-                )}
-              </View>
-            )}
-          </View>
-          <Text
-            style={{
-              ...styles.text,
-              ...textStyle,
-              marginLeft: 25,
-              marginRight: 5,
-              fontSize: 14,
-            }}
-          >
-            {integerToCurrency(amount)}
-          </Text>
-        </ListItem>
-      </Button>
-    );
-  }
-}
+          )}
+        </View>
+        <Text
+          style={{
+            ...styles.text,
+            ...textStyle,
+            marginLeft: 25,
+            marginRight: 5,
+            fontSize: 14,
+          }}
+        >
+          {integerToCurrency(amount)}
+        </Text>
+      </ListItem>
+    </Button>
+  );
+});
 
-export class TransactionList extends Component {
-  makeData = memoizeOne(transactions => {
+export function TransactionList({
+  accounts,
+  categories,
+  payees,
+  transactions,
+  showCategory,
+  isNew,
+  onSelect,
+  scrollProps = {},
+  onLoadMore,
+}) {
+  const sections = useMemo(() => {
     // Group by date. We can assume transactions is ordered
     const sections = [];
     transactions.forEach(transaction => {
@@ -1026,78 +1331,70 @@ export class TransactionList extends Component {
       }
     });
     return sections;
-  });
+  }, [transactions]);
 
-  render() {
-    const { transactions, scrollProps = {}, onLoadMore } = this.props;
-
-    const sections = this.makeData(transactions);
-
-    return (
-      <>
-        {scrollProps.ListHeaderComponent}
-        <ListBox
-          {...scrollProps}
-          aria-label="transaction list"
-          label=""
-          loadMore={onLoadMore}
-          selectionMode="none"
-        >
-          {sections.length === 0 ? (
-            <Section>
-              <Item textValue="No transactions">
-                <div
-                  style={{
-                    display: 'flex',
-                    justifyContent: 'center',
-                    width: '100%',
-                    backgroundColor: theme.mobilePageBackground,
-                  }}
-                >
-                  <Text style={{ fontSize: 15 }}>No transactions</Text>
-                </div>
-              </Item>
-            </Section>
-          ) : null}
-          {sections.map(section => {
-            return (
-              <Section
-                title={
-                  <span>
-                    {monthUtils.format(section.date, 'MMMM dd, yyyy')}
-                  </span>
-                }
-                key={section.id}
+  return (
+    <>
+      {scrollProps.ListHeaderComponent}
+      <ListBox
+        {...scrollProps}
+        aria-label="transaction list"
+        label=""
+        loadMore={onLoadMore}
+        selectionMode="none"
+      >
+        {sections.length === 0 ? (
+          <Section>
+            <Item textValue="No transactions">
+              <div
+                style={{
+                  display: 'flex',
+                  justifyContent: 'center',
+                  width: '100%',
+                  backgroundColor: theme.mobilePageBackground,
+                }}
               >
-                {section.data.map((transaction, index, transactions) => {
-                  return (
-                    <Item
-                      key={transaction.id}
-                      style={{
-                        fontSize:
-                          index === transactions.length - 1 ? 98 : 'inherit',
-                      }}
-                      textValue={transaction.id}
-                    >
-                      <Transaction
-                        transaction={transaction}
-                        categories={this.props.categories}
-                        accounts={this.props.accounts}
-                        payees={this.props.payees}
-                        showCategory={this.props.showCategory}
-                        added={this.props.isNew(transaction.id)}
-                        onSelect={this.props.onSelect} // onSelect(transaction)}
-                      />
-                    </Item>
-                  );
-                })}
-              </Section>
-            );
-          })}
-        </ListBox>
-      </>
-    );
-  }
+                <Text style={{ fontSize: 15 }}>No transactions</Text>
+              </div>
+            </Item>
+          </Section>
+        ) : null}
+        {sections.map(section => {
+          return (
+            <Section
+              title={
+                <span>{monthUtils.format(section.date, 'MMMM dd, yyyy')}</span>
+              }
+              key={section.id}
+            >
+              {section.data.map((transaction, index, transactions) => {
+                return (
+                  <Item
+                    key={transaction.id}
+                    style={{
+                      fontSize:
+                        index === transactions.length - 1 ? 98 : 'inherit',
+                    }}
+                    textValue={transaction.id}
+                  >
+                    <Transaction
+                      transaction={transaction}
+                      categories={categories}
+                      accounts={accounts}
+                      payees={payees}
+                      showCategory={showCategory}
+                      added={isNew(transaction.id)}
+                      onSelect={onSelect} // onSelect(transaction)}
+                    />
+                  </Item>
+                );
+              })}
+            </Section>
+          );
+        })}
+      </ListBox>
+    </>
+  );
 }
 
 function ListBox(props) {
@@ -1203,7 +1500,6 @@ function Option({ isLast, item, state }) {
   const { optionProps, isSelected } = useOption({ key: item.key }, state, ref);
 
   // Determine whether we should show a keyboard
-  // focus ring for accessibility
   const { isFocusVisible, focusProps } = useFocusRing();
 
   return (
diff --git a/packages/desktop-client/src/components/util/AmountInput.tsx b/packages/desktop-client/src/components/util/AmountInput.tsx
index 632082cf9eac2ddf10e8e4d62b7ab24b2d5dba21..9adec5f3adb4384cecb84beaa480dc06eee4a5ad 100644
--- a/packages/desktop-client/src/components/util/AmountInput.tsx
+++ b/packages/desktop-client/src/components/util/AmountInput.tsx
@@ -1,4 +1,10 @@
-import React, { type Ref, useRef, useState, useEffect } from 'react';
+import React, {
+  type Ref,
+  useRef,
+  useState,
+  useEffect,
+  type FocusEventHandler,
+} from 'react';
 
 import evalArithmetic from 'loot-core/src/shared/arithmetic';
 import { amountToInteger } from 'loot-core/src/shared/util';
@@ -15,69 +21,75 @@ import useFormat from '../spreadsheet/useFormat';
 type AmountInputProps = {
   id?: string;
   inputRef?: Ref<HTMLInputElement>;
-  initialValue: number;
+  value: number;
   zeroSign?: '-' | '+';
-  onChange?: (value: number) => void;
-  onBlur?: () => void;
+  onChange?: (value: string) => void;
+  onFocus?: FocusEventHandler<HTMLInputElement>;
+  onBlur?: FocusEventHandler<HTMLInputElement>;
+  onUpdate?: (amount: number) => void;
   style?: CSSProperties;
   textStyle?: CSSProperties;
   focused?: boolean;
+  disabled?: boolean;
 };
 
 export function AmountInput({
   id,
   inputRef,
-  initialValue,
+  value: initialValue,
   zeroSign = '-', // + or -
-  onChange,
+  onFocus,
   onBlur,
+  onChange,
+  onUpdate,
   style,
   textStyle,
   focused,
+  disabled = false,
+  ...props
 }: AmountInputProps) {
   const format = useFormat();
-  const [negative, setNegative] = useState(
-    (initialValue === 0 && zeroSign === '-') || initialValue < 0,
-  );
+  const negative = (initialValue === 0 && zeroSign === '-') || initialValue < 0;
 
-  const initialValueAbsolute = format(Math.abs(initialValue), 'financial');
+  const initialValueAbsolute = format(Math.abs(initialValue || 0), 'financial');
   const [value, setValue] = useState(initialValueAbsolute);
   useEffect(() => setValue(initialValueAbsolute), [initialValueAbsolute]);
 
   const buttonRef = useRef();
+  const ref = useRef<HTMLInputElement>();
+  const mergedRef = useMergedRefs<HTMLInputElement>(inputRef, ref);
+
+  useEffect(() => {
+    if (focused) {
+      ref.current?.focus();
+    }
+  }, [focused]);
 
   function onSwitch() {
-    setNegative(!negative);
-    fireChange(value, !negative);
+    fireUpdate(!negative);
   }
 
-  function fireChange(val, neg) {
+  function getAmount(negate) {
     const valueOrInitial = Math.abs(
-      amountToInteger(evalArithmetic(val, initialValueAbsolute)),
+      amountToInteger(evalArithmetic(value, initialValueAbsolute)),
     );
-    const amount = neg ? valueOrInitial * -1 : valueOrInitial;
-
-    onChange?.(amount);
+    return negate ? valueOrInitial * -1 : valueOrInitial;
   }
 
-  function onInputAmountChange(value) {
-    setValue(value ? value : '');
+  function onInputTextChange(val) {
+    setValue(val ? val : '');
+    onChange?.(val);
   }
 
-  const ref = useRef<HTMLInputElement>();
-  const mergedRef = useMergedRefs<HTMLInputElement>(inputRef, ref);
-
-  useEffect(() => {
-    if (focused) {
-      ref.current?.focus();
-    }
-  }, [focused]);
+  function fireUpdate(negate) {
+    onUpdate?.(getAmount(negate));
+  }
 
   function onInputAmountBlur(e) {
-    fireChange(value, negative);
     if (!ref.current?.contains(e.relatedTarget)) {
-      onBlur?.();
+      fireUpdate(negative);
     }
+    onBlur?.(e);
   }
 
   return (
@@ -88,6 +100,7 @@ export function AmountInput({
       leftContent={
         <Button
           type="bare"
+          disabled={disabled}
           aria-label={`Make ${negative ? 'positive' : 'negative'}`}
           style={{ padding: '0 7px' }}
           onPointerUp={onSwitch}
@@ -102,17 +115,18 @@ export function AmountInput({
         </Button>
       }
       value={value}
+      disabled={disabled}
       focused={focused}
       style={{ flex: 1, alignItems: 'stretch', ...style }}
       inputStyle={{ paddingLeft: 0, ...textStyle }}
       onKeyUp={e => {
         if (e.key === 'Enter') {
-          fireChange(value, negative);
-          onBlur?.();
+          fireUpdate(negative);
         }
       }}
-      onUpdate={onInputAmountChange}
+      onUpdate={onInputTextChange}
       onBlur={onInputAmountBlur}
+      onFocus={onFocus}
     />
   );
 }
@@ -124,8 +138,8 @@ export function BetweenAmountInput({ defaultValue, onChange }) {
   return (
     <View style={{ flexDirection: 'row', alignItems: 'center' }}>
       <AmountInput
-        initialValue={num1}
-        onChange={value => {
+        value={num1}
+        onUpdate={value => {
           setNum1(value);
           onChange({ num1: value, num2 });
         }}
@@ -133,8 +147,8 @@ export function BetweenAmountInput({ defaultValue, onChange }) {
       />
       <View style={{ margin: '0 5px' }}>and</View>
       <AmountInput
-        initialValue={num2}
-        onChange={value => {
+        value={num2}
+        onUpdate={value => {
           setNum2(value);
           onChange({ num1, num2: value });
         }}
diff --git a/packages/desktop-client/src/hooks/useSingleActiveEditForm.tsx b/packages/desktop-client/src/hooks/useSingleActiveEditForm.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..662c3c3fcbb39e7e54f4c5408755ca1e2b73296e
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useSingleActiveEditForm.tsx
@@ -0,0 +1,125 @@
+import React, {
+  type ReactNode,
+  createContext,
+  useContext,
+  useState,
+  useRef,
+  useEffect,
+} from 'react';
+
+import usePrevious from './usePrevious';
+import useStableCallback from './useStableCallback';
+
+type ActiveEditCleanup = () => void;
+type ActiveEditAction = () => void | ActiveEditCleanup;
+
+type SingleActiveEditFormContextValue = {
+  formName: string;
+  editingField: string;
+  onRequestActiveEdit: (
+    field: string,
+    action?: ActiveEditAction,
+    clearActiveEditDelayMs?: number,
+  ) => void;
+  onClearActiveEdit: (delayMs?: number) => void;
+};
+
+const SingleActiveEditFormContext = createContext<
+  SingleActiveEditFormContextValue | undefined
+>(undefined);
+
+type SingleActiveEditFormProviderProps = {
+  formName: string;
+  children: ReactNode;
+};
+
+export function SingleActiveEditFormProvider({
+  formName,
+  children,
+}: SingleActiveEditFormProviderProps) {
+  const [editingField, setEditingField] = useState(null);
+  const prevEditingField = usePrevious(editingField);
+  const actionRef = useRef<ActiveEditAction>(null);
+  const cleanupRef = useRef<ActiveEditCleanup | void>(null);
+
+  useEffect(() => {
+    if (prevEditingField != null && prevEditingField !== editingField) {
+      runCleanup();
+    } else if (prevEditingField == null && editingField !== null) {
+      runAction();
+    }
+  }, [editingField]);
+
+  const runAction = () => {
+    cleanupRef.current = actionRef.current?.();
+  };
+
+  const runCleanup = () => {
+    const editCleanup = cleanupRef.current;
+    if (typeof editCleanup === 'function') {
+      editCleanup?.();
+    }
+    cleanupRef.current = null;
+  };
+
+  const onClearActiveEdit = (delayMs?: number) => {
+    setTimeout(() => setEditingField(null), delayMs);
+  };
+
+  const onRequestActiveEdit = useStableCallback(
+    (
+      field: string,
+      action: ActiveEditAction,
+      options: {
+        clearActiveEditDelayMs?: number;
+      },
+    ) => {
+      if (editingField === field) {
+        // Already active.
+        return;
+      }
+
+      if (editingField) {
+        onClearActiveEdit(options?.clearActiveEditDelayMs);
+      } else {
+        actionRef.current = action;
+        setEditingField(field);
+      }
+    },
+  );
+
+  return (
+    <SingleActiveEditFormContext.Provider
+      value={{
+        formName,
+        editingField,
+        onRequestActiveEdit,
+        onClearActiveEdit,
+      }}
+    >
+      {children}
+    </SingleActiveEditFormContext.Provider>
+  );
+}
+
+type UseSingleActiveEditFormResult = {
+  formName: SingleActiveEditFormContextValue['formName'];
+  editingField?: SingleActiveEditFormContextValue['editingField'];
+  onRequestActiveEdit: SingleActiveEditFormContextValue['onRequestActiveEdit'];
+  onClearActiveEdit: SingleActiveEditFormContextValue['onClearActiveEdit'];
+};
+
+export function useSingleActiveEditForm(): UseSingleActiveEditFormResult | null {
+  const context = useContext(SingleActiveEditFormContext);
+
+  if (!context) {
+    return null;
+  }
+
+  return {
+    formName: context.formName,
+    editingField: context.editingField,
+    onRequestActiveEdit: context.onRequestActiveEdit,
+    onClearActiveEdit: context.onClearActiveEdit,
+  };
+}
diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts
index 1c12c19f09d4d07da5b681ae27deee0be03ee95d..a5b0396c404d4f81fa16f1b5d69448989431571f 100644
--- a/packages/loot-core/src/client/state-types/modals.d.ts
+++ b/packages/loot-core/src/client/state-types/modals.d.ts
@@ -90,6 +90,7 @@ type FinanceModals = {
   'edit-field': {
     name: string;
     onSubmit: (name: string, value: string) => void;
+    onClose: () => void;
   };
 
   'budget-summary': {
diff --git a/upcoming-release-notes/2068.md b/upcoming-release-notes/2068.md
new file mode 100644
index 0000000000000000000000000000000000000000..a4afd516438adc2c103cf7f6250585ec4ca9e43e
--- /dev/null
+++ b/upcoming-release-notes/2068.md
@@ -0,0 +1,6 @@
+---
+category: Features
+authors: [joel-jeremy]
+---
+
+Mobile split transactions