From 0286fa4ed08404edaa6a2d4950d973b07de192e6 Mon Sep 17 00:00:00 2001
From: Jed Fox <git@jedfox.com>
Date: Fri, 30 Jun 2023 17:01:43 -0400
Subject: [PATCH] Remove usage of Formik (#1212)

---
 packages/desktop-client/package.json          |   1 -
 .../desktop-client/src/components/common.tsx  |   6 +-
 .../src/components/modals/CloseAccount.js     | 250 +++++++++---------
 .../components/modals/CreateLocalAccount.js   | 236 ++++++++---------
 upcoming-release-notes/1212.md                |   6 +
 yarn.lock                                     |  41 +--
 6 files changed, 247 insertions(+), 293 deletions(-)
 create mode 100644 upcoming-release-notes/1212.md

diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json
index 79c5020dc..bfcd44fa6 100644
--- a/packages/desktop-client/package.json
+++ b/packages/desktop-client/package.json
@@ -30,7 +30,6 @@
     "debounce": "^1.2.0",
     "downshift": "7.6.0",
     "focus-visible": "^4.1.1",
-    "formik": "^0.11.10",
     "glamor": "^2.20.40",
     "hotkeys-js": "3.10.3",
     "inter-ui": "^3.19.3",
diff --git a/packages/desktop-client/src/components/common.tsx b/packages/desktop-client/src/components/common.tsx
index e7c4fe5a8..63af5c81b 100644
--- a/packages/desktop-client/src/components/common.tsx
+++ b/packages/desktop-client/src/components/common.tsx
@@ -129,11 +129,11 @@ type LinkProps = ComponentProps<typeof Button>;
 export function LinkButton({ style, children, ...nativeProps }: LinkProps) {
   return (
     <Button
-      {...css(
+      style={[
         {
           textDecoration: 'none',
           color: styles.textColor,
-          backgroundColor: 'transparent !important',
+          backgroundColor: 'transparent',
           display: 'inline',
           border: 0,
           cursor: 'pointer',
@@ -149,7 +149,7 @@ export function LinkButton({ style, children, ...nativeProps }: LinkProps) {
         },
         styles.smallText,
         style,
-      )}
+      ]}
       {...nativeProps}
     >
       {children}
diff --git a/packages/desktop-client/src/components/modals/CloseAccount.js b/packages/desktop-client/src/components/modals/CloseAccount.js
index 9e0356461..b6e5ddddf 100644
--- a/packages/desktop-client/src/components/modals/CloseAccount.js
+++ b/packages/desktop-client/src/components/modals/CloseAccount.js
@@ -1,7 +1,5 @@
 import React, { useState } from 'react';
 
-import { Formik } from 'formik';
-
 import { integerToCurrency } from 'loot-core/src/shared/util';
 
 import { colors } from '../../style';
@@ -52,6 +50,11 @@ function CloseAccount({
   modalProps,
 }) {
   let [loading, setLoading] = useState(false);
+  let [transfer, setTransfer] = useState('');
+  let [category, setCategory] = useState('');
+
+  let [transferError, setTransferError] = useState(false);
+  let [categoryError, setCategoryError] = useState(false);
 
   let filtered = accounts.filter(a => a.id !== account.id);
   let onbudget = filtered.filter(a => a.offbudget === 0);
@@ -79,153 +82,138 @@ function CloseAccount({
               </span>
             )}
           </P>
-          <Formik
-            validateOnChange={false}
-            initialValues={{ transfer: '', category: '' }}
-            onSubmit={(values, { setErrors }) => {
-              const errors = {};
-              if (balance !== 0 && values.transfer === '') {
-                errors.transfer = 'required';
-              }
-              if (
-                needsCategory(account, values.transfer, accounts) &&
-                values.category === ''
-              ) {
-                errors.category = 'required';
-              }
-              setErrors(errors);
+          <form
+            onSubmit={event => {
+              event.preventDefault();
+
+              let transferError = balance !== 0 && transfer === '';
+              setTransferError(transferError);
+
+              let categoryError =
+                needsCategory(account, transfer, accounts) && category === '';
+              setCategoryError(categoryError);
 
-              if (Object.keys(errors).length === 0) {
+              if (!transferError && !categoryError) {
                 setLoading(true);
 
                 actions
-                  .closeAccount(
-                    account.id,
-                    values.transfer || null,
-                    values.category || null,
-                  )
+                  .closeAccount(account.id, transfer || null, category || null)
                   .then(() => {
                     modalProps.onClose();
                   });
               }
             }}
-            render={({
-              values,
-              errors,
-              touched,
-              handleChange,
-              handleBlur,
-              handleSubmit,
-              isSubmitting,
-              setFieldValue,
-            }) => (
-              <form onSubmit={handleSubmit}>
-                {balance !== 0 && (
-                  <View>
+          >
+            {balance !== 0 && (
+              <View>
+                <P>
+                  This account has a balance of{' '}
+                  <strong>{integerToCurrency(balance)}</strong>. To close this
+                  account, select a different account to transfer this balance
+                  to:
+                </P>
+
+                <Select
+                  value={transfer}
+                  onChange={event => {
+                    setTransfer(event.target.value);
+                    if (transferError && event.target.value) {
+                      setTransferError(false);
+                    }
+                  }}
+                  style={{ width: 200, marginBottom: 15 }}
+                >
+                  <option value="">Select account...</option>
+                  <optgroup label="For Budget">
+                    {onbudget.map(acct => (
+                      <option key={acct.id} value={acct.id}>
+                        {acct.name}
+                      </option>
+                    ))}
+                  </optgroup>
+
+                  <optgroup label="Off Budget">
+                    {offbudget.map(acct => (
+                      <option key={acct.id} value={acct.id}>
+                        {acct.name}
+                      </option>
+                    ))}
+                  </optgroup>
+                </Select>
+                {transferError && (
+                  <FormError style={{ marginBottom: 15 }}>
+                    Transfer is required
+                  </FormError>
+                )}
+
+                {needsCategory(account, transfer, accounts) && (
+                  <View style={{ marginBottom: 15 }}>
                     <P>
-                      This account has a balance of{' '}
-                      <strong>{integerToCurrency(balance)}</strong>. To close
-                      this account, select a different account to transfer this
-                      balance to:
+                      Since you are transferring the balance from a budgeted
+                      account to an off-budget account, this transaction must be
+                      categorized. Select a category:
                     </P>
 
-                    <Select
-                      name="transfer"
-                      value={values.transfer}
-                      onChange={handleChange}
-                      onBlur={handleBlur}
-                      style={{ width: 200, marginBottom: 15 }}
-                    >
-                      <option value="">Select account...</option>
-                      <optgroup label="For Budget">
-                        {onbudget.map(acct => (
-                          <option key={acct.id} value={acct.id}>
-                            {acct.name}
-                          </option>
-                        ))}
-                      </optgroup>
-
-                      <optgroup label="Off Budget">
-                        {offbudget.map(acct => (
-                          <option key={acct.id} value={acct.id}>
-                            {acct.name}
-                          </option>
-                        ))}
-                      </optgroup>
-                    </Select>
-                    {errors.transfer && (
-                      <FormError style={{ marginBottom: 15 }}>
-                        Transfer is required
-                      </FormError>
+                    <CategorySelect
+                      categoryGroups={categoryGroups}
+                      value={category}
+                      onChange={event => {
+                        setCategory(event.target.value);
+                        if (categoryError && event.target.value) {
+                          setCategoryError(false);
+                        }
+                      }}
+                      style={{ width: 200 }}
+                    />
+                    {categoryError && (
+                      <FormError>Category is required</FormError>
                     )}
-
-                    {needsCategory(account, values.transfer, accounts) && (
-                      <View style={{ marginBottom: 15 }}>
-                        <P>
-                          Since you are transferring the balance from a budgeted
-                          account to an off-budget account, this transaction
-                          must be categorized. Select a category:
-                        </P>
-
-                        <CategorySelect
-                          categoryGroups={categoryGroups}
-                          name="category"
-                          value={values.category}
-                          onChange={handleChange}
-                          onBlur={handleBlur}
-                          style={{ width: 200 }}
-                        />
-                        {errors.category && (
-                          <FormError>Category is required</FormError>
-                        )}
-                      </View>
-                    )}
-                  </View>
-                )}
-
-                {!canDelete && (
-                  <View style={{ marginBottom: 15 }}>
-                    <Text style={{ fontSize: 12 }}>
-                      You can also{' '}
-                      <LinkButton
-                        onClick={() => {
-                          setLoading(true);
-
-                          actions
-                            .forceCloseAccount(account.id)
-                            .then(() => modalProps.onClose());
-                        }}
-                        style={{ color: colors.r6 }}
-                      >
-                        force close
-                      </LinkButton>{' '}
-                      the account which will delete it and all its transactions
-                      permanently. Doing so may change your budget unexpectedly
-                      since money in it may vanish.
-                    </Text>
                   </View>
                 )}
+              </View>
+            )}
 
-                <View
-                  style={{
-                    flexDirection: 'row',
-                    justifyContent: 'flex-end',
-                  }}
-                >
-                  <Button
-                    type="submit"
-                    style={{ marginRight: 10 }}
-                    onClick={modalProps.onClose}
+            {!canDelete && (
+              <View style={{ marginBottom: 15 }}>
+                <Text style={{ fontSize: 12 }}>
+                  You can also{' '}
+                  <LinkButton
+                    onClick={() => {
+                      setLoading(true);
+
+                      actions
+                        .forceCloseAccount(account.id)
+                        .then(() => modalProps.onClose());
+                    }}
+                    style={{ color: colors.r6 }}
                   >
-                    Cancel
-                  </Button>
-                  <Button type="submit" primary>
-                    Close Account
-                  </Button>
-                </View>
-              </form>
+                    force close
+                  </LinkButton>{' '}
+                  the account which will delete it and all its transactions
+                  permanently. Doing so may change your budget unexpectedly
+                  since money in it may vanish.
+                </Text>
+              </View>
             )}
-          />
+
+            <View
+              style={{
+                flexDirection: 'row',
+                justifyContent: 'flex-end',
+              }}
+            >
+              <Button
+                type="submit"
+                style={{ marginRight: 10 }}
+                onClick={modalProps.onClose}
+              >
+                Cancel
+              </Button>
+              <Button type="submit" primary>
+                Close Account
+              </Button>
+            </View>
+          </form>
         </View>
       )}
     </Modal>
diff --git a/packages/desktop-client/src/components/modals/CreateLocalAccount.js b/packages/desktop-client/src/components/modals/CreateLocalAccount.js
index 449e7435d..b4dd83a4a 100644
--- a/packages/desktop-client/src/components/modals/CreateLocalAccount.js
+++ b/packages/desktop-client/src/components/modals/CreateLocalAccount.js
@@ -1,8 +1,6 @@
-import React from 'react';
+import React, { useState } from 'react';
 import { useNavigate } from 'react-router-dom';
 
-import { Formik } from 'formik';
-
 import { toRelaxedNumber } from 'loot-core/src/shared/util';
 
 import { colors } from '../../style';
@@ -21,140 +19,142 @@ import {
 
 function CreateLocalAccount({ modalProps, actions }) {
   let navigate = useNavigate();
+  let [name, setName] = useState('');
+  let [offbudget, setOffbudget] = useState(false);
+  let [balance, setBalance] = useState('0');
+
+  let [nameError, setNameError] = useState(false);
+  let [balanceError, setBalanceError] = useState(false);
+
+  let validateBalance = balance => !isNaN(parseFloat(balance));
+
   return (
     <Modal title="Create Local Account" {...modalProps} showBack={false}>
       {() => (
         <View>
-          <Formik
-            validateOnChange={false}
-            initialValues={{ name: '', balance: '0' }}
-            validate={() => ({})}
-            onSubmit={async (values, { setErrors }) => {
-              const errors = {};
-              if (!values.name) {
-                errors.name = 'required';
-              }
-              if (isNaN(parseFloat(values.balance))) {
-                errors.balance = 'format';
-              }
-              setErrors(errors);
+          <form
+            onSubmit={async event => {
+              event.preventDefault();
+
+              let nameError = !name;
+              setNameError(nameError);
 
-              if (Object.keys(errors).length === 0) {
+              let balanceError = !validateBalance(balance);
+              setBalanceError(balanceError);
+
+              if (!nameError && !balanceError) {
                 modalProps.onClose();
                 let id = await actions.createAccount(
-                  values.name,
-                  toRelaxedNumber(values.balance),
-                  values.offbudget,
+                  name,
+                  toRelaxedNumber(balance),
+                  offbudget,
                 );
                 navigate('/accounts/' + id);
               }
             }}
-            render={({
-              values,
-              errors,
-              touched,
-              handleChange,
-              handleBlur,
-              handleSubmit,
-              isSubmitting,
-              setFieldValue,
-            }) => (
-              <form onSubmit={handleSubmit}>
-                <InlineField label="Name" width="75%">
-                  <InitialFocus>
-                    <Input
-                      name="name"
-                      value={values.name}
-                      onChange={handleChange}
-                      onBlur={handleBlur}
-                      style={{ flex: 1 }}
-                    />
-                  </InitialFocus>
-                </InlineField>
-                {errors.name && (
-                  <FormError style={{ marginLeft: 75 }}>
-                    Name is required
-                  </FormError>
-                )}
+          >
+            <InlineField label="Name" width="75%">
+              <InitialFocus>
+                <Input
+                  name="name"
+                  value={name}
+                  onChange={event => setName(event.target.value)}
+                  onBlur={event => {
+                    let name = event.target.value.trim();
+                    setName(name);
+                    if (name && nameError) {
+                      setNameError(false);
+                    }
+                  }}
+                  style={{ flex: 1 }}
+                />
+              </InitialFocus>
+            </InlineField>
+            {nameError && (
+              <FormError style={{ marginLeft: 75 }}>Name is required</FormError>
+            )}
 
-                <View
+            <View
+              style={{
+                width: '75%',
+                flexDirection: 'row',
+                justifyContent: 'flex-end',
+              }}
+            >
+              <View style={{ flexDirection: 'column' }}>
+                <label
                   style={{
-                    width: '75%',
-                    flexDirection: 'row',
+                    userSelect: 'none',
+                    textAlign: 'right',
+                    width: '100%',
+                    display: 'flex',
+                    verticalAlign: 'center',
                     justifyContent: 'flex-end',
                   }}
+                  htmlFor="offbudget"
                 >
-                  <View style={{ flexDirection: 'column' }}>
-                    <label
-                      style={{
-                        userSelect: 'none',
-                        textAlign: 'right',
-                        width: '100%',
-                        display: 'flex',
-                        verticalAlign: 'center',
-                        justifyContent: 'flex-end',
-                      }}
-                      htmlFor="offbudget"
-                    >
-                      <input
-                        id="offbudget"
-                        name="offbudget"
-                        type="checkbox"
-                        checked={!!values.offbudget}
-                        onChange={handleChange}
-                        onBlur={handleBlur}
-                      />
-                      Off-budget
-                    </label>
-                    <div
-                      style={{
-                        textAlign: 'right',
-                        fontSize: '0.7em',
-                        color: colors.n5,
-                        marginTop: 3,
-                      }}
-                    >
-                      <Text>
-                        This cannot be changed later. <br /> {'\n'}
-                        See{' '}
-                        <ExternalLink
-                          linkColor="muted"
-                          to="https://actualbudget.org/docs/accounts/#off-budget-accounts"
-                        >
-                          Accounts Overview
-                        </ExternalLink>{' '}
-                        for more information.
-                      </Text>
-                    </div>
-                  </View>
-                </View>
-
-                <InlineField label="Balance" width="75%">
-                  <Input
-                    name="balance"
-                    value={values.balance}
-                    onChange={handleChange}
-                    onBlur={handleBlur}
-                    style={{ flex: 1 }}
+                  <input
+                    id="offbudget"
+                    name="offbudget"
+                    type="checkbox"
+                    checked={offbudget}
+                    onChange={event => setOffbudget(event.target.checked)}
                   />
-                </InlineField>
-                {errors.balance && (
-                  <FormError style={{ marginLeft: 75 }}>
-                    Balance must be a number
-                  </FormError>
-                )}
+                  Off-budget
+                </label>
+                <div
+                  style={{
+                    textAlign: 'right',
+                    fontSize: '0.7em',
+                    color: colors.n5,
+                    marginTop: 3,
+                  }}
+                >
+                  <Text>
+                    This cannot be changed later. <br /> {'\n'}
+                    See{' '}
+                    <ExternalLink
+                      linkColor="muted"
+                      to="https://actualbudget.org/docs/accounts/#off-budget-accounts"
+                    >
+                      Accounts Overview
+                    </ExternalLink>{' '}
+                    for more information.
+                  </Text>
+                </div>
+              </View>
+            </View>
 
-                <ModalButtons>
-                  <Button onClick={() => modalProps.onBack()} type="button">
-                    Back
-                  </Button>
-                  <Button primary style={{ marginLeft: 10 }}>
-                    Create
-                  </Button>
-                </ModalButtons>
-              </form>
+            <InlineField label="Balance" width="75%">
+              <Input
+                name="balance"
+                value={balance}
+                onChange={event => setBalance(event.target.value)}
+                onBlur={event => {
+                  let balance = event.target.value.trim();
+                  setBalance(balance);
+                  if (validateBalance(balance) && balanceError) {
+                    setBalanceError(false);
+                  }
+                }}
+                style={{ flex: 1 }}
+              />
+            </InlineField>
+            {balanceError && (
+              <FormError style={{ marginLeft: 75 }}>
+                Balance must be a number
+              </FormError>
             )}
-          />
+
+            <ModalButtons>
+              <Button onClick={() => modalProps.onBack()} type="button">
+                Back
+              </Button>
+              <Button primary style={{ marginLeft: 10 }}>
+                Create
+              </Button>
+            </ModalButtons>
+          </form>
         </View>
       )}
     </Modal>
diff --git a/upcoming-release-notes/1212.md b/upcoming-release-notes/1212.md
new file mode 100644
index 000000000..25c41c422
--- /dev/null
+++ b/upcoming-release-notes/1212.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [j-f1]
+---
+
+Remove usage of Formik
diff --git a/yarn.lock b/yarn.lock
index 139d497ee..bbff0d89b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -98,7 +98,6 @@ __metadata:
     debounce: ^1.2.0
     downshift: 7.6.0
     focus-visible: ^4.1.1
-    formik: ^0.11.10
     glamor: ^2.20.40
     hotkeys-js: 3.10.3
     inter-ui: ^3.19.3
@@ -9410,21 +9409,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"formik@npm:^0.11.10":
-  version: 0.11.11
-  resolution: "formik@npm:0.11.11"
-  dependencies:
-    lodash.clonedeep: ^4.5.0
-    lodash.isequal: 4.5.0
-    lodash.topath: 4.5.2
-    prop-types: ^15.5.10
-    warning: ^3.0.0
-  peerDependencies:
-    react: ">=15"
-  checksum: ce0fde6f1d87fa60afe3a025904e64b6394dccb48dd0739cd1d3aa987b00bad2e15584b03db7ed284b9260748db14fb36459f4768e7b93548e091223c406a0b8
-  languageName: node
-  linkType: hard
-
 "forwarded@npm:0.2.0":
   version: 0.2.0
   resolution: "forwarded@npm:0.2.0"
@@ -12257,13 +12241,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lodash.clonedeep@npm:^4.5.0":
-  version: 4.5.0
-  resolution: "lodash.clonedeep@npm:4.5.0"
-  checksum: 92c46f094b064e876a23c97f57f81fbffd5d760bf2d8a1c61d85db6d1e488c66b0384c943abee4f6af7debf5ad4e4282e74ff83177c9e63d8ff081a4837c3489
-  languageName: node
-  linkType: hard
-
 "lodash.debounce@npm:^4.0.8":
   version: 4.0.8
   resolution: "lodash.debounce@npm:4.0.8"
@@ -12278,7 +12255,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lodash.isequal@npm:4.5.0, lodash.isequal@npm:^4.5.0":
+"lodash.isequal@npm:^4.5.0":
   version: 4.5.0
   resolution: "lodash.isequal@npm:4.5.0"
   checksum: da27515dc5230eb1140ba65ff8de3613649620e8656b19a6270afe4866b7bd461d9ba2ac8a48dcc57f7adac4ee80e1de9f965d89d4d81a0ad52bb3eec2609644
@@ -12306,13 +12283,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lodash.topath@npm:4.5.2":
-  version: 4.5.2
-  resolution: "lodash.topath@npm:4.5.2"
-  checksum: 04583e220f4bb1c4ac0008ff8f46d9cb4ddce0ea1090085790da30a41f4cb1b904d885cb73257fca619fa825cd96f9bb97c67d039635cb76056e18f5e08bfdee
-  languageName: node
-  linkType: hard
-
 "lodash.uniq@npm:^4.5.0":
   version: 4.5.0
   resolution: "lodash.uniq@npm:4.5.0"
@@ -18573,15 +18543,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"warning@npm:^3.0.0":
-  version: 3.0.0
-  resolution: "warning@npm:3.0.0"
-  dependencies:
-    loose-envify: ^1.0.0
-  checksum: c9f99a12803aab81b29858e7dc3415bf98b41baee3a4c3acdeb680d98c47b6e17490f1087dccc54432deed5711a5ce0ebcda2b27e9b5eb054c32ae50acb4419c
-  languageName: node
-  linkType: hard
-
 "warning@npm:^4.0.3":
   version: 4.0.3
   resolution: "warning@npm:4.0.3"
-- 
GitLab