Kami melakukan rebranding Slash sebagai bagian dari strategi kami. Penggalangan dana Seri B, dan kami ingin menyegarkan tampilan produk kami bersamaan dengan merek kami. Kami segera menyadari bahwa selain memperbarui antarmuka pengguna (UI), ini adalah kesempatan yang tepat untuk mengatasi sebagian besar utang teknis yang telah kami akumulasikan selama tiga tahun terakhir dalam proses pertumbuhan yang sangat cepat.

Oleh karena itu, proyek Peremajaan wajah lahir - sebuah kesempatan bagi kami untuk membangun fondasi frontend Slash untuk bisnis dan tim kami yang sedang berkembang. Kami ingin mengambil keputusan yang akan bertahan lama dengan belajar dari kesalahan masa lalu kami.

Bagian 1: Mengatasi utang teknis

Pada bagian ini, saya akan membahas setiap poin terkait "utang". Pada bagian 2, saya akan membahas secara mendalam tentang solusi yang kami tawarkan untuk setiap poin tersebut.

Masalah Kinerja: Bottleneck pada Runtime CSS dalam JavaScript

Sistem desain asli kami yang dibangun di atas emosi dan telah melayani kami dengan baik. Keuntungan utama dari perpustakaan CSS-in-JS adalah penempatan bersama markup dan styling komponen, yang membuat proses iterasi menjadi sangat cepat. Namun, seiring dengan pertumbuhan pelanggan kami dan volume data yang semakin besar, kami mulai mengalami bottleneck kinerja di Emotion. CSS-in-JS memerlukan overhead runtime dan dapat memperlambat loop acara utama. Hal ini terasa saat memvirtualisasikan daftar besar dan menggulir dengan cepat. Saat komponen dipasang dan dilepas dengan cepat, perhitungan gaya secara dinamis dan pembangkitan lembar gaya berdampak pada browser hingga tidak dapat merender pada 60fps. Perusahaan lain telah menemukan hambatan kinerja yang serupa dan beralih dari solusi yang bergantung pada beban runtime.

Komponen "Tuhan" berbasis properti

Konsep asli dari “Allah menentangHal ini berasal dari OOP, di mana sebuah objek akan menangani terlalu banyak tanggung jawab sendirian. Kami memperhatikan hal serupa terjadi pada komponen React kami, yang kami mulai sebut sebagai "God components".

Banyak dari komponen React asli kami ditulis dengan antarmuka berbasis props, di mana Anda menyediakan data dan callback, lalu merender satu komponen. Secara permukaan, ini memberikan antarmuka yang elegan: cukup sediakan data dan props, lalu percayakan komponen untuk menangani semua markup dan logika bisnis.

Dalam praktiknya, seiring dengan pertumbuhan basis kode, komponen-komponen ini diperluas untuk mencakup penggunaan yang awalnya tidak dipertimbangkan atau direncanakan. Setiap kali seorang insinyur membutuhkan konfigurasi baru, mereka akan menambahkan props, yang seiring waktu membuat pemeliharaan dan penggunaan menjadi lebih sulit. Komponen-komponen ini akhirnya menjadi "God components" yang memerlukan perawatan khusus untuk dipelihara, dengan props satu kali yang dimaksudkan untuk menyelesaikan masalah tertentu, atau "escape hatches" yang dimaksudkan untuk menyisipkan/mengganti markup tambahan ke dalam komponen itu sendiri (serupa dengan pola render prop). Saya akan menampilkan contoh kode di bagian selanjutnya dari blog ini.

Pengelolaan formulir yang tidak konsisten

Solusi manajemen state asli kami hanyalah manajemen state React. State lokal mudah digunakan, tetapi kurang standar dan keamanan tipe yang mendalam, yang mengakibatkan kualitas kode yang tidak konsisten. Cara setiap insinyur mengimplementasikan state lokal akan sedikit berbeda - beberapa akan memanfaatkan... API Konteks dengan satu keadaan formulir bersama yang besar, sementara yang lain akan memecah bidang-bidang tersebut menjadi individu. gunakanState panggilan.

Ketika tiba waktunya untuk memperpanjang formulir-formulir ini, seringkali metadata seperti Apakah bidang telah disentuh? akan ditambahkan, menciptakan komponen yang berantakan dengan banyak state lokal yang didefinisikan secara eksplisit untuk menangani interaksi formulir yang seharusnya distandardisasi.

Bagian 2: Pelaksanaan

Sekarang setelah kita telah mengidentifikasi kelemahan-kelemahan saat ini dalam kode basis kita, saya akan membahas secara mendalam solusi-solusi yang ada dan proses pengambilan keputusan di balik masing-masing solusi tersebut.

Migrasi ke Tailwind v4.0

Saat mengevaluasi solusi styling, kami memiliki dua finalis: Tailwind v4.0 dan PandaCSS, karena keduanya tidak memiliki overhead runtime dan memungkinkan penempatan bersama markup dan styling komponen. Keuntungan PandaCSS adalah keamanan tipe dan kurva pembelajaran yang rendah, karena sistem desain yang sudah ada terlihat sangat mirip.

Keunggulan Tailwind adalah ekosistem yang matang dan familiaritas pengembang. Banyak insinyur kami telah menggunakan Tailwind untuk proyek pribadi dan sangat menyukainya, termasuk saya sendiri. Ada alat-alat fantastis dengan dukungan Tailwind bawaan seperti Supernova dan Varian TailwindAlat pemrograman AI juga sangat handal dalam menghasilkan nama kelas Tailwind secara otomatis, sehingga proses migrasi menjadi jauh lebih mudah.

Untuk memperlihatkan keunggulan kinerja dengan tidak adanya overhead runtime, berikut ini adalah grafik flame sebelum dan sesudah dari operasi yang sama (menggulir dengan cepat pada tampilan tabel virtual yang besar), di Emotion dan Tailwind:

Emotion memperkenalkan beban overhead injeksi gaya runtime.
Tailwind tidak mengalami kendala-kendala ini!

Pipeline Figma ke Kode

Saat beralih ke Tailwind, kami ingin cara untuk menjaga token Figma dan basis kode kami tetap sinkron, dengan Figma sebagai sumber kebenaran kami. Sistem desain kami di Figma dikelola oleh agensi desain kami, Metakarbon, yang membuat keputusan desain dan membuat lembar gaya dengan token semantik.

Tujuan kami adalah memiliki alur kerja di mana setiap perubahan pada sistem desain dasar dapat ditinjau dan diimplementasikan secara manual dengan usaha minimal. Kami memilih Supernova untuk mengekstrak token desain dari Figma ke dalam kode, karena mereka memiliki ekstrakor kode Tailwind v4.0 yang sangat baik.

Saat bekerja dengan output CSS kami, kami menemui masalah yang menarik: Kami ingin agar token semantik mengacu pada nilai yang berbeda, tergantung pada properti yang diterapkan padanya. Berikut ini adalah contoh ilustratif:

/* Raw Supernova output */
@theme {
	--color-bg-neutral-subtle-default: var(--color-light-product-neutral-100);
	--color-border-neutral-subtle-default: var(--color-light-product-neutral-500);
	--color-text-neutral-subtle-default: var(--color-light-product-neutral-1000);
}

Kami ingin netral-halus-default untuk menghasilkan 3 warna yang berbeda produk ringan-netral: 100, 500 dan 1000 - Namun, jika kita hanya menggunakan ini, Tailwind v4.0 --warna variabel tema akan menghasilkan bg-bg-netral-halus-default, Border latar belakang netral dan halus (default), bg-text-netral-halus-default dan banyak nama kelas lainnya yang tidak masuk akal dan berlebihan dalam penamaan.

Oleh karena itu, kami menulis skrip build khusus untuk mengubah output kami agar dapat memanfaatkan fitur yang lebih spesifik dari Tailwind v4.0. Nama ruang nama variabel tema:

/* Build script output */
@theme {
	--background-color-neutral-subtle-default: var(--color-light-product-neutral-100);
  --border-color-neutral-subtle-default: var(--color-light-product-neutral-500);
  --text-color-neutral-subtle-default: var(--color-light-product-neutral-1000);
  
  /* Additional properties that we want to have the same values as other tokens */
  --divide-color-neutral-subtle-default: var(--color-light-product-neutral-500);
}

Yang menghasilkan nama kelas seperti bg-netral-halus-default, Batas netral, halus, default dan teks netral-halus-default - yang semuanya memiliki nuansa yang berbeda-beda.

Jadi, seiring dengan perubahan sistem desain dasar kami, kami hanya perlu menjalankan eksportir Supernova kami dan memasukkan outputnya ke dalam basis kode kami untuk direview. Kemudian, skrip build kami secara otomatis mengonversi output mentah menjadi berkas CSS yang sudah dibangun, yang digunakan oleh semua paket kami.

Good to know

At Slash, we follow a philosophy of maintaining control over our tooling . This allows us to extend functionality quickly and prevent vendor lock in. It also helps us diagnose and fix issues directly. By having our build script, we aren’t locked into Supernova as a vendor, and can extend it - like adding support for divide to be the same color as border, or light vs dark mode themes.

Other examples include our query builder and JSON schema to typescript code generation pipeline.

Perpustakaan antarmuka pengguna tanpa antarmuka (Headless UI): Base UI dan React Aria

Pertama-tama, saya ingin menjelaskan mengapa kami memilih perpustakaan antarmuka pengguna (UI) tanpa gaya (headless) daripada solusi yang sudah diberi gaya. Tujuan utama dari pembaruan antarmuka ini adalah agar identitas merek kami dapat diekspresikan dengan jelas melalui produk kami - dan menggunakan solusi yang sudah diberi gaya seperti Shadcn atau MaterialUI akan menghilangkan tujuan ini dengan memberi gaya pada komponen-komponen kami secara default.

Sebelum pembaruan antarmuka, kami menggunakan Radix UI, tetapi kami tidak yakin tentang masa depannyaSeperti yang kita lihat, para pengembang inti telah beralih ke Base UI. Base UI telah muncul sebagai perpustakaan yang sederhana dan dapat diperluas, yang sangat sesuai dengan kebutuhan kita, dengan kepatuhan yang baik terhadap aksesibilitas. Dengan pengembang yang sangat baik, Base UI memberi kita keyakinan bahwa di masa depan, perpustakaan ini akan berkembang sejalan dengan kebutuhan Slash.

Untuk komponen tertentu yang belum didukung oleh Base UI (misalnya, pemilih tanggal kami), kami memilih React Aria, yang menjadi pilihan kedua kami dalam pencarian antarmuka pengguna tanpa kepala (headless UI). React Aria telah teruji dalam praktik dan mematuhi semua standar aksesibilitas dengan ketat. Pada akhirnya, kami membangun komponen kami di atas Base UI karena pendekatan React Aria cukup kaku dan mengharuskan kami sepenuhnya mengadopsi ekosistem React Aria, sementara kami lebih memilih sesuatu yang lebih ringan secara mental.

Komponen Komposit

Seperti yang telah disebutkan sebelumnya, komponen berbasis props awalnya elegan, tetapi memiliki keterbatasan dalam hal skalabilitas. Solusi kami untuk menciptakan komponen yang lebih skalabel adalah dengan mengandalkan secara intensif pada... arsitektur komponen gabungan, yang menyerahkan kendali markup komponen kepada orangtuanya. Cara terbaik untuk menjelaskan perbedaan antara keduanya adalah dengan contoh - berikut adalah versi sederhana dari komponen select yang dapat dicari versi lama dan baru kami:

// Old SearchableSelect implememtation
type Props<T> = {
  selection?: string;
  setSelection?: (val: string) => void;
  
  // Escape hatches added over time
  shouldSetIsChangedFromTextRefAfterTableViewUpdate?: boolean;
  customTextSelectionLogic?: boolean;
  preloadOptions?: boolean;
  hideDropdownOnEmptyOptions?: boolean;
  renderSelectedItem?: (item: T) => ReactNode;
  hideRightIcon?: boolean;
  isClearable?: boolean;
  
  // ... more props
};

export function SearchableSingleSelect<T>(props: Props<T>) {
  const {
    customTextSelectionLogic = false,
    preloadOptions = false,
    hideDropdownOnEmptyOptions = false,
    renderSelectedItem,
    hideRightIcon = false,
    isClearable = true,
    shouldSetIsChangedFromTextRefAfterTableViewUpdate = true,
    // ... more props
  } = props;

  return (
    <FormElementWrapper
      renderInput={(props) => (
        <MenuBase
          setSelection={(value) => {
            setSelection(value);
            
            // Escape hatch for custom behavior
            if (customTextSelectionLogic) {
              return;
            }
            
            // Different logic based on renderSelectedItem prop
            if (value && !renderSelectedItem) {
              setTextInput(findLabel(value));
            } else if (value && renderSelectedItem) {
              setTextInput('');
            }
          }}
        >
          {/* Conditional rendering based on prop */}
          {renderSelectedItem && selectedItem ? (
            <CustomRenderedItem item={selectedItem} />
          ) : null}
          
          <TextInput value={textInput} onChange={...} />
        </MenuBase>
      )}
      {/* Nested ternaries based on multiple props */}
      {...(hideRightIcon ? {} : {
        rightIcon: () => {
          return !isClearable ? (
            <ChevronIcon />
          ) : textInput?.length || (renderSelectedItem && selectedItem) ? (
            <ClearButton onClick={clearSelection} />
          ) : (
            <ChevronIcon onClick={toggleDropdown} />
          );
        }
      })}
    />
  );
}
// Old SearchableSelect usage
<SearchableSingleSelect
  selection={selection}
  setSelection={setSelection}
  tableManager={tableManager}
  isClearable={true}
  customTextSelectionLogic={false}
  hideDropdownOnEmptyOptions={false}
  renderSelectedItem={(item) => <CustomView item={item} />}
  shouldSetIsChangedFromTextRefAfterTableViewUpdate={true}
  // ... more props
/>

Dan inilah komponen select yang dapat dicari kami yang baru, yang menggunakan arsitektur komponen gabungan:

// New SearchableSelect implementation

// Context manages shared state
const SearchableSelectContext = createContext<{
  selectState: SingleSelectState;
  tableManager: TableManagerState;
}>(null);

// Root sets up context
const Root = ({ children, tableManager, type }) => {
  const selectState = useSingleSelectState();
  
  return (
    <SearchableSelectContext.Provider value={{ selectState, tableManager }}>
      <MenuV2.Root>{children}</MenuV2.Root>
    </SearchableSelectContext.Provider>
  );
};

// EachItem exposes items with callbacks
const EachItem = ({ children }) => {
  const { selectState, tableManager } = useSearchableSelectContext();
  
  const items = tableManager.data.map((item, index) => ({
    item,
    callbacks: {
      onClick: (item) => selectState.toggleItem(item.key, index),
      getIsSelected: (item) => selectState.isItemSelected(item.key),
    },
  }));

  return <>{items.map(children)}</>;
};

// SearchBar handles its own state
const SearchBar = () => {
  const { tableManager } = useSearchableSelectContext();
  const [textInput, setTextInput] = useState('');

  useEffect(() => {
    tableManager.updateSearch(textInput);
  }, [textInput]);

  return <InputV2.Field value={textInput} onChange={e => setTextInput(e.target.value)} />;
};

// ... Other components: Content, DefaultTrigger, Item
// New SearchableSelect usage
<SearchableSelect.Root type="single" tableManager={tableManager}>
  <SearchableSelect.DefaultTrigger>
    Select an option...
  </SearchableSelect.DefaultTrigger>
  
  <SearchableSelect.Content>
    <SearchableSelect.SearchBar />
    
    <SearchableSelect.EachItem>
      {({ item, callbacks }) => (
        <MenuV2.Item onClick={() => callbacks.onClick(item)}>
          <CustomView item={item} />
        </MenuV2.Item>
      )}
    </SearchableSelect.EachItem>
  </SearchableSelect.Content>
</SearchableSelect.Root>

Pada pandangan pertama, komponen gabungan tampak lebih rumit - pengembang perlu memahami pola markup dan mengikutinya. Keuntungannya adalah fleksibilitas dalam memperluas markup, karena kendali penuh ada pada komponen induk. Oleh karena itu, modifikasi markup tidak pernah dimasukkan ke dalam komponen inti. Jika seorang insinyur perlu menambahkan footer ke elemen select? Cukup tambahkan ke markup - tidak perlu... Render Pilihan Footer Tambahkan ke komponen inti! Jika footer menjadi persyaratan umum, buatlah sebuah <SearchableSelect.Footer /> komponen!

Salah satu jebakan umum dalam komponen gabungan adalah insinyur dapat menduplikasi implementasi pola umum yang tidak terdapat dalam komponen inti. Oleh karena itu, tim kami perlu lebih sadar akan pola umum ini dan menambahkannya ke dalam komponen inti jika diperlukan. Anda juga dapat menggunakan komponen gabungan sebagai antarmuka API dasar untuk komponen satu baris - tetapi kami melakukannya dengan sengaja agar komponen-komponen ini tidak menjadi "god components". Saya akan membahas lebih detail tentang cara kami menangani berbagi pengetahuan melalui Storybook di bagian selanjutnya.

Varian Tailwind

Dalam proses mendesain komponen-komponen kami, kami sangat bergantung pada Varian TailwindBeberapa dari kita telah menggunakan Otoritas Variasi Kelas (CVA) dan kami menginginkan logika variasi semacam itu dalam solusi styling kami. Tailwind Variants memiliki Beberapa fitur yang membuat kami memutuskan untuk menggunakannya., antarmuka pemrograman aplikasi (API) slot dan mekanisme penyelesaian konflik bawaan merupakan yang utama.

Pada akhirnya, kami hanya membutuhkan solusi yang dapat menjaga kode variasi gaya kami tetap rapi dan cukup fleksibel untuk mencakup pendekatan komponen gabungan kami. Penggunaan Tailwind Variants secara signifikan mengstandarkan kode gaya kondisional lama kami, yang secara eksplisit menerapkan token berbeda pada elemen div yang menggunakan gaya emosi berdasarkan propertinya.

Berikut ini adalah contoh ilustratif tentang implementasi dan penggunaan tombol kami:

import { tv } from 'tailwind-variants';

const buttonVariants = tv({
  // Slots API - different parts of the button component
  slots: {
    base: [
      'inline-flex',
      'items-center',
      'justify-center',
      'gap-1',
      'group',
    ],
    hoverOverlay: [
      'opacity-0',
      'absolute',
      'inset-0',
      'group-hover:group-enabled:opacity-100',
      'pointer-events-none',
    ],
  },

  // Variants - different visual options.
  variants: {
    color: {
      cta: {
        base: [
          'border',
          'border-neutral-boldest-default',
          'bg-button-cta-default',
          'shadow-cta-neutral-default',
          
          'hover:border-neutral-bolder-hover',
          'hover:bg-button-cta-hover',
          'hover:text-neutral-bolder-hover',
        ],
      },
      ghost: {
        base: [
          'bg-transparent',
          'text-neutral-normal-default',
          
          'hover:bg-neutral-subtle-hover',
          'hover:text-neutral-normal-hover',
        ],
      },
    },
    size: {
      sm: {
        base: [
          'h-6',
          'px-1.5',
          'typography-button-small',
          'rounded-border-radius-sm',
        ],
      },
      md: {
        base: [
          'h-8',
          'px-2',
          'typography-button-medium',
          'rounded-border-radius-md',
        ],
      },
    },
  },

  // Compound Variants - styles when multiple variants are combined
  compoundVariants: [
    {
      // Ghost buttons with small size get reduced padding
      color: 'ghost',
      size: 'sm',
      class: {
        base: 'px-1',
      },
    },
    {
      // CTA buttons at large size get enhanced shadows
      color: 'cta',
      size: 'lg',
      class: {
        base: 'shadow-cta-neutral-prominent',
      },
    },
  ],

  // Default Variants - applied when no variant is specified
  defaultVariants: {
    color: 'neutral',
    size: 'md',
    disabled: false,
  },
});

// Extract variant props type from buttonVariants
type ButtonVariantProps = VariantProps<typeof buttonVariants>;

// Button component props
interface ButtonProps extends ButtonVariantProps {
  children?: React.ReactNode;
  onClick?: () => void;
  as?: ElementType;
  className?: string;
}

// Button component implementation
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ 
    children, 
    color, 
    size, 
    disabled, 
    onClick, 
    as: Component = 'button',
    className,
    ...rest 
  }, ref) => {
    // Generate slot classes based on variant props
    const slots = buttonVariants({ color, size, disabled });
    
    return (
      <Component
        ref={ref}
        className={slots.base({ className })}
        onClick={onClick}
        disabled={disabled}
        {...rest}
      >
        {/* Hover overlay slot */}
        <span className={slots.hoverOverlay()} />
        
        {/* Button content */}
        {children}
      </Component>
    );
  }
);
// End usage example
<Button color="cta" size="lg">
  Large CTA Button
</Button>

Seperti yang dapat Anda lihat, penelepon dari Tombol Sebenarnya tidak perlu menggunakan Varian Tombol - ia meneruskan properti ke Tombol sebagai opsi konfigurasi, yang kemudian digunakan untuk memanggil Varian Tombol secara internal. Kami juga memanfaatkan perpanjang Fitur untuk berbagi gaya antara komponen serupa, seperti bidang input dan bidang select.

Anda juga akan melihat betapa mudahnya memindai gaya-gaya tersebut berkat nama kelas semantik kami.

Pengujian dan dokumentasi: Storybook + Chromatic

Pada paruh pertama proyek, saya bekerja sendirian dan tidak memberikan perhatian yang cukup pada pengujian dan dokumentasi seperti yang seharusnya. Namun, setelah saya menyelesaikan perpustakaan komponen dasar dan lebih banyak insinyur bergabung untuk memperbarui produk masing-masing, semakin jelas bahwa kita membutuhkan tempat terpusat untuk menjawab pertanyaan-pertanyaan sederhana seperti “apakah kita memiliki komponen ini?” dan “bagaimana cara menggunakan komponen ini dengan benar?”.

Salah satu insinyur baru kami, Sam, secara mandiri berhasil mengatur rangkaian pengujian Storybook dan Chromatic. Tanpa ini, kami bisa dengan cepat terjebak dalam perangkap utang teknis akibat penggunaan komponen yang tidak tepat - terutama mengingat kurva pembelajaran yang curam dari komponen gabungan.

Storybook memungkinkan kita untuk mengumpulkan contoh penggunaan untuk semua komponen, sehingga pertanyaan-pertanyaan tersebut dapat dijawab secara lengkap dengan contoh kode. Hal ini juga membuat jelas secara langsung komponen mana yang harus digunakan oleh seorang insinyur, serta apakah komponen tersebut mendukung opsi konfigurasi yang mereka butuhkan.

Chromatic mendeteksi perubahan visual, sehingga setiap perubahan pada komponen dasar akan langsung ditandai jika mengubah tampilan visual - menghentikan penggabungan hingga direview secara manual.

Pengelolaan formulir: Zod vs ArkType

Kode dasar Slash ditulis sepenuhnya dalam Typescript, dan kami memanfaatkan hal tersebut secara maksimal dengan menghasilkan tipe bersama di seluruh kode dasar kami pada saat kompilasi. Kami menginginkan solusi manajemen formulir yang membuat pengembang sulit untuk melakukan kesalahan yang merugikan diri sendiri, dan memanfaatkan keamanan tipe yang luas yang kami miliki.

Kami akhirnya memilih ArkType, karena sangat terintegrasi dengan Typescript, kami dapat mencegah terjadinya penyimpangan apa pun. Kami dapat mengambil tipe yang dihasilkan dan menghubungkannya secara satu-satu dengan tipe ArkType menggunakan memuaskansehingga jika tipe dasar kita berubah, kode kita akan menimbulkan kesalahan tipe.

Berikut ini adalah contoh ilustratif:

# models/entity/Address.yaml
title: Address
type: object
properties:
  addressLine:
    type: string
  addressLine2:
    type: string
  addressCity:
    type: string
  addressState:
    type: string
  addressCountry:
    type: string
    minLength: 2
    maxLength: 2
    description: ISO 3166-1 Alpha-2 Country Code (e.g., US, FR, UK, DE, ...).
required:
  - addressLine
  - addressCity
  - addressState
// shemas/entity/Address.ts

// Schema implementation of an address model
import type { entity } from '@slashfi/models';
import { type Type, type } from 'arktype';

// Helper to configure default error messages
export const nonEmptyString = (fieldName: string) =>
  type('string>0').configure({
    message: `${fieldName} is required.`,
  });

export const Address = type({
  addressLine: nonEmptyString('Address Line'),
  addressLine2: 'string | undefined',
  addressCity: nonEmptyString('Address City'),
  addressState: nonEmptyString('Address State'),
  addressCountry: type('string==2 | undefined').configure({
    message: 'Country code must be a 2-letter ISO 3166-1 Alpha-2 code',
  }),
}) satisfies Type<entity.Address>;
// entity.Address is built automatically from our YAML spec

export const defaultAddress: typeof Address.infer = { ... }
// Illustrative form component that submits an address - using Tanstack form
const AddressForm = () => {
  const form = useAppForm({
    defaultValues: defaultAddress,
    // Validate the form input on mount and on change
    validators: {
      onMount: Address,
      onChange: Address,
    },
    onSubmit: ({ value }: { value: Address }) => {
      // Submit the value
      console.log(value);
    },
  });

  return (
    <form.AppForm>
      <form.Field name="addressLine">
        {(field) => (
          <>
            <Input
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {/* Use built in metadata like isTouched */}
            {field.state.meta.isTouched ? (
              <p>{getMessage(field.state.meta.errors)}</p>
            ) : null}
          </>
        )}
      </form.Field>

      {/* ... more inputs for each field in address */}

      {/* Subscribe for reactive form update */}
      <form.Subscribe
        selector={({ canSubmit, isSubmitting }) => ({
          canSubmit,
          isSubmitting,
        })}
      >
        {({ canSubmit, isSubmitting }) => (
          <Button loading={isSubmitting} disabled={canSubmit}>
            Create vendor
          </Button>
        )}
      </form.Subscribe>
    </form.AppForm>
  );
};

Meskipun Zod dan React Hook Form dapat menjadi pilihan yang baik, kami menginginkan sesuatu yang secara langsung memanfaatkan TypeScript, dan ArkType jelas menjadi pilihan utama di sini. Kami memilih Tanstack Form daripada React Hook Form karena kepatuhannya yang lebih ketat terhadap TypeScript.

Salah satu hal yang akan Anda perhatikan dengan Tanstack form adalah bagaimana ia memaksa Anda untuk mengikuti struktur formulir yang ketat dan terstruktur. Meskipun hal ini menimbulkan kurva pembelajaran, kami merasa hal ini sepadan untuk memastikan standarisasi dalam cara kami menulis formulir di seluruh basis kode kami.

Implementasi: Fitur bendera dan cabang kode

Ketika sampai pada tahap penulisan kode untuk Facelift, kami ingin menghindari satu pull request besar, karena kami masih merilis fitur-fitur baru dalam sistem desain lama bersamaan dengan Facelift.

Kami menggunakan sebuah flag fitur, dan hanya mengaktifkan fitur flag untuk akun kami saja, memungkinkan kami untuk makanan anjing Perubahan kami sendiri. Kami juga mengimplementasikan toolbar sederhana untuk mengaktifkan dan menonaktifkan facelift, sehingga sangat mudah untuk mendeteksi regresi dengan cepat mengaktifkan dan menonaktifkan facelift.

Tombol toggle yang akan kita gunakan untuk dengan cepat mengaktifkan dan menonaktifkan pembaruan tampilan (facelift) guna mendeteksi regresi.

Adapun mengenai pembagian kode sebenarnya, kami berupaya untuk tidak menduplikasi kode logika bisnis, dengan cara sebagai berikut:

const CardsPage = () => {
  const { isFacelift } = useFaceliftToolbar();
  
	// Existing business logic
	const cards = useCards()
  
  if (isFacelift) {
	  // New, facelifted components
	  return (
		  <CardsTableV2 cards={cards} />
	  )
  }

	
	// Existing UI
	return (
		<CardsTable cards={cards} />
	)
}

Contoh di atas menggambarkan kasus ideal. - Kenyataan memperkenalkan sejumlah besar kode baru ke dalam basis kode yang sudah ada jauh lebih rumit. Misalnya: jika Anda melakukan... Apakah Facelift? Jika Anda mengubah pengaturan tingkat atas (misalnya di tingkat halaman utama), Anda harus menduplikasi semua logika bisnis untuk semua cabang di bawah komponen tersebut. Namun, jika Anda melakukan pemeriksaan di tingkat komponen individu, Anda terikat pada logika markup lama dari komponen induk, yang seringkali memerlukan pembaruan tampilan.

Yang melindungi kami dari masalah produksi adalah menyembunyikan semua fungsionalitas ini di balik fitur flag sehingga bagi semua pelanggan sebenarnya, Apakah Facelift? selalu salah Sampai kami siap. Secara internal, kami akan tetap mengaktifkannya agar dapat mendeteksi regresi saat mereka masuk ke platform.

Seiring dengan selesainya pengembangan fitur facelift, kami bekerja sama dengan pelanggan yang memiliki hubungan dekat dengan kami dan mengaktifkan fitur tersebut untuk mereka dengan opsi untuk menonaktifkannya, sambil meminta umpan balik dan mendeteksi regresi tanpa mengganggu alur kerja yang sudah ada.

Catatan tentang pelaksanaan

Meskipun bagian di atas disusun dengan cara yang sangat teratur, hal ini sama sekali tidak mencerminkan urutan implementasi yang sebenarnya. Pada kenyataannya, keputusan-keputusan ini diambil melalui proses mencoba berbagai hal dan melakukan iterasi dengan cepat. Misalnya,

  • Draf pertama skrip build CSS kami membanjiri @utilitas Petunjuk untuk membuat setiap nama kelas kustom yang kami inginkan - kemudian Sam mengembangkan lebih lanjut dengan menemukan ini Diskusi GitHub yang menjelaskan tema tailwind dan ruang nama variabel.
  • Ada 2 iterasi dari komponen pemilihan tunggal yang dapat dicari kami yang menggunakan komponen BaseUI yang berbeda sebelum kami memutuskan untuk menggunakan pendekatan saat ini, yaitu menggunakan Menu API.

Intinya, kami tidak membuat keputusan-keputusan ini dengan sebaik-baiknya seperti yang dijelaskan dalam pertimbangan di atas. Benang merahnya adalah iterasi yang terus-menerus tanpa terjebak dalam rasa perlu untuk mendapatkan hasil yang sempurna sejak awal.

Bagian 3: Pelajaran yang Diperoleh

Saya secara pribadi belajar banyak dari sebuah proyek di mana lingkupnya mencakup seluruh frontend. Tim kami juga belajar banyak, terutama karena semakin banyak insinyur yang berkontribusi dalam memperbarui produk masing-masing.

“Meningkatkan standar minimum”

Salah satu alasan utama mengapa utang teknis menumpuk sejak awal adalah karena kurangnya pengetahuan yang dibagikan. Di startup yang masih muda, ini bukan masalah, karena semua orang bekerja sangat dekat satu sama lain. Namun, seiring pertumbuhan tim dan perluasan cakupan produk, menjadi tidak mungkin untuk berbagi asumsi dan pola penggunaan secepat sebelumnya.

Filosofi kami dalam melakukan pembaruan antarmuka pengguna (facelift) adalah untuk "meningkatkan standar dasar". Kami ingin membuatnya sulit untuk menulis kode frontend yang buruk, yang berarti mendokumentasikan seperti apa kode frontend yang baik dan mendeteksi kesalahan sebelum masuk ke produksi. Storybook dan Chromatic sangat penting untuk ini, dan investasi untuk mengimplementasikannya sangat sepadan. Setiap upaya yang Anda lakukan untuk meningkatkan standar dasar akan memberikan dampak positif yang berlipat ganda pada kualitas kode di seluruh basis kode - dan sebaliknya juga berlaku.

Kenyataan proyek jangka panjang

Proses pembaruan antarmuka dari awal hingga akhir memakan waktu sekitar 4 bulan kerja terus-menerus, dengan "hack week" khusus di akhir yang berlangsung selama 3 minggu dan melibatkan 6 insinyur lain yang turut berkontribusi dalam pembaruan antarmuka (terima kasih banyak kepada mereka <3). Saya juga memiliki Manajer Proyek (PM) yang sangat baik (Andy) yang membantu merumuskan lingkup proyek secara keseluruhan, berkomunikasi dengan Metacarbon, dan memberikan masukan tentang pola antarmuka pengguna (UX) baru.

Karena pembaruan antarmuka pengguna (facelift) bukanlah proyek yang bisa saya rilis secara bertahap, saya tidak merasakan kepuasan merilis produk kepada pelanggan dalam waktu yang sangat lama. Saya belajar bahwa pendekatan yang baik adalah beralih konteks secara internal—kadang-kadang saya bekerja pada perpustakaan komponen inti, lalu menerapkan komponen inti tersebut pada halaman lengkap, dan bergantian antara keduanya. Pendekatan ini juga membantu Anda dengan cepat mendeteksi kasus khusus dalam penggunaan komponen inti, memaksa Anda untuk kembali dan membuat garis abstraksi yang lebih baik.

Menyusun ulang ribuan baris kode tanpa ada hasil yang terlihat bagi pelanggan bisa sangat menyurutkan semangat. Yang membuat saya tetap termotivasi adalah beberapa hal:

  1. Datang ke kantor setiap hari masih menyenangkan karena orang-orang di Slash!
  2. Saya masih bisa memamerkan kemajuan secara internal. Mendapatkan semacam dorongan motivasi dari menunjukkan kemajuan yang Anda buat kepada orang lain adalah cara yang bagus untuk tetap termotivasi, terutama jika Anda mencintai pekerjaan Anda.
  3. Proyek ini direncanakan untuk menjadi dasar bagi basis kode frontend kami untuk jangka waktu yang lama ke depan, jadi sangat penting untuk melakukannya dengan benar.
  4. Slash sedang berkembang dan meraih kemenangan, yang merupakan SECARA KESELURUHAN motivasi.

Bagian 4: Apa yang akan terjadi selanjutnya?

Dengan peluncuran pembaruan antarmuka pengguna kami yang sudah berjalan lancar, prioritas utama kami kini adalah mengidentifikasi kasus-kasus khusus dan regresi. Beruntungnya, kami memiliki tim dukungan kelas dunia yang membantu pelanggan kami melewati transisi ini dan mengidentifikasi masalah dengan cepat.

Jika Anda ingin melihat karya kami, Anda dapat melihatnya di situs web kami. situs demo, di mana Anda juga dapat melihat fitur-fitur yang ditawarkan oleh Slash!

Jika Anda sampai di sini dan merupakan insinyur berbakat yang ingin memecahkan masalah untuk pelanggan nyata di startup yang sedang berkembang pesat, silakan ajukan lamaran. di siniKami sangat senang mendengar kabar dari Anda.

Read more from us