Skip to content

fernandocamargo/money

Repository files navigation

Money Component

A revolutionary approach to React component design: Composition over Configuration

The Problem

Traditional React components for complex UI elements often fall into the "prop hell" trap:

// The old way: prop explosion
<DatePicker
  showClearDates={true}
  showDefaultInputIcon={true}
  customInputIcon={<Icon />}
  customArrowIcon={<Icon />}
  customCloseIcon={<Icon />}
  displayFormat="MM/DD/YYYY"
  monthFormat="MMMM YYYY"
  phrases={{
    closeDatePicker: 'Close',
    clearDates: 'Clear',
    // ... 20+ more phrases
  }}
  renderCalendarInfo={() => <div>Info</div>}
  renderDayContents={(day) => day.format('D')}
  renderMonthElement={({ month }) => month}
  // ... dozens more props
/>

This approach has critical flaws:

  • API Explosion: Every customization requires a new prop
  • Maintenance Nightmare: Component logic becomes a maze of conditionals
  • Poor Scalability: Adding features means adding props and complexity
  • Limited Flexibility: Only what props allow, nothing more
  • Testing Complexity: Combinatorial explosion of prop permutations
  • Bundle Size: All features shipped to all users, always

The Solution

This project demonstrates a fundamentally different approach: provide atomic building blocks, let consumers compose them.

Instead of accepting dozens of formatting props, the Money component:

  1. Decomposes money values into atomic fragments (operator, currency, symbol, number)
  2. Provides pre-rendered elements and component factories
  3. Delegates presentation to lightweight formatter components
  4. Maintains a minimal, stable API that never grows

Architecture

Core Money Component

The Money component does one thing exceptionally well: decomposition.

<Money currency="EUR" format={CustomFormatter}>1234.56</Money>

Internally, it:

  1. Parses the value using Intl.NumberFormat (locale-aware)
  2. Extracts atomic fragments:
    • operator: "+" or "-"
    • currency: "EUR"
    • symbol: "€"
    • number: "1,234.56"
  3. Creates bound component factories for each fragment type
  4. Generates pre-rendered React elements
  5. Passes everything to the formatter

Key insight: The Money component contains zero presentation logic. It's purely a data transformation pipeline.

Note

Historical Context: When this component was originally conceived, the Intl.NumberFormat.prototype.formatToParts() API didn't exist yet. The current implementation uses RegExp to parse the formatted string and extract fragments (symbol, number, etc.).

If this were to be maintained or published as a package, migrating to formatToParts() would be a natural improvement - it provides a much more ergonomic way to decompose formatted numbers without parsing:

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
}).formatToParts(1234.56)
// Returns: [
//   { type: 'currency', value: '$' },
//   { type: 'integer', value: '1,234' },
//   { type: 'decimal', value: '.' },
//   { type: 'fraction', value: '56' }
// ]

However, the core architectural principle—composition over configuration—remains valuable regardless of the parsing implementation.

Formatters: Simple Composition

Formatters receive all the building blocks and simply arrange them:

// MoneyJustNumbersFormatter.js - 15 lines total
const MoneyJustNumbersFormatter = ({
  components: { container: Container },
  elements: { operator, number },
  negative,
}) => {
  const children = useMemo(  // Memoized for performance
    () => [negative && operator, number],
    [negative, operator, number]
  );

  return <Container>{children}</Container>;
};

That's it. No prop parsing, no conditionals, no complexity. Just composition.

Real-World Example

Before (Traditional Approach)

// Component implementation: 300+ lines
// Props needed: 25+
// Conditional branches: 15+
<MoneyDisplay
  value={1234.56}
  currency="EUR"
  locale="en-US"
  showSymbol={true}
  showCurrencyCode={true}
  symbolPosition="before"
  currencyPosition="after"
  showOperator={false}
  operatorPosition="before"
  abbreviateLargeNumbers={false}
  abbreviationThreshold={1000}
  abbreviationUnits={['k', 'M', 'B']}
  granularDigits={false}
  colorCodeMagnitudes={false}
  magnitudeColors={{ thousand: '#blue', million: '#green' }}
  decimalPlaces={2}
  thousandsSeparator=","
  decimalSeparator="."
  negativeFormat="minus"
  // ... and more
/>

After (Composition Approach)

// Six built-in formatters, each 15-40 lines
// Core component: 104 lines, zero presentation logic
// Props needed: 4 (value, currency, locale, format)

// Default format
<Money currency="EUR">1234.56</Money>
// Output: +€ EUR 1,234.56

// Just numbers
<Money currency="EUR" format={MoneyJustNumbersFormatter}>1234.56</Money>
// Output: +1,234.56

// Abbreviated
<Money currency="EUR" format={MoneyRoundedFormatter}>1234.56</Money>
// Output: +€ 1.2k

// Custom format (create your own in minutes)
const CustomFormatter = ({ elements: { symbol, number } }) => (
  <span className="price">{symbol}{number}</span>
);

<Money currency="EUR" format={CustomFormatter}>1234.56</Money>
// Output: €1,234.56

Built-in Formatters

  1. MoneyDefaultFormatter - Symbol + currency code
  2. MoneyJustCurrencyFormatter - Currency code only
  3. MoneyJustSymbolFormatter - Symbol only
  4. MoneyJustNumbersFormatter - Numbers only
  5. MoneyRoundedFormatter - Abbreviated (1.2k, 3.5M)
  6. MoneyGranularElementsFormatter - Fine-grained per-digit styling (see advanced example below)

Each formatter is 15-88 lines. Each is independently testable. Each is tree-shakeable.

Why This Matters

Scalability

Adding a new format variant:

  • Traditional approach: Add 3-5 props, add conditional logic, update tests, increase bundle size
  • This approach: Create a 20-line formatter component

Flexibility

Want to color each digit differently based on magnitude? Want to animate the transition between values? Want to add tooltips to specific parts?

  • Traditional approach: Request new props from maintainer, wait for release
  • This approach: Write a custom formatter, takes 10 minutes

Maintainability

  • Traditional approach: Core component grows with every feature request
  • This approach: Core component never changes, formatters are isolated

Bundle Size

  • Traditional approach: All features shipped always, ~50kb+
  • This approach: Import only what you need, ~5kb + chosen formatters

Testing

  • Traditional approach: Test all prop combinations (exponential complexity)
  • This approach: Test core component once, test each formatter in isolation

Advanced Example: Granular Per-Digit Styling

The MoneyGranularElementsFormatter demonstrates the true power of this compositional approach. It does something that would be nearly impossible with traditional props: style each digit group independently based on magnitude.

The Problem It Solves

Want to:

  • Make cents smaller and underlined?
  • Color-code digits by magnitude (thousands gray, millions blue, billions yellow)?
  • Add hover effects to specific digit groups?
  • Animate transitions between magnitude levels?
  • Add tooltips explaining each group?

Traditional approach: You'd need dozens of props like centsStyle, hundredsStyle, thousandsStyle, millionsStyle, centsClassName, thousandsClassName, onHundredsHover, etc. The combinatorial explosion makes this impractical.

This approach: One formatter component that semantically marks up the structure, then style with CSS.

How It Works

The formatter decomposes the number into atomic fragments and tags each with semantic attributes:

<Money currency="USD" format={MoneyGranularElementsFormatter}>
  1234567.89
</Money>

Generates this HTML:

<span class="container">
  <span class="symbol">$</span>
  <span class="currency">USD</span>
  <span class="number">
    <span class="fragment" type="integer" subtype="million">1</span>
    <span class="fragment" type="separator-integer">,</span>
    <span class="fragment" type="integer" subtype="thousand">234</span>
    <span class="fragment" type="separator-integer">,</span>
    <span class="fragment" type="integer" subtype="hundred">567</span>
    <span class="fragment" type="separator-decimal">.</span>
    <span class="fragment" type="decimal">89</span>
  </span>
</span>

Semantic Attributes

Each fragment receives:

type attribute:

  • decimal - The fractional part (cents)
  • separator-decimal - The decimal point
  • integer - Integer digit groups
  • separator-integer - Thousands separators

subtype attribute (for integers):

  • hundred - 1-999
  • thousand - 1,000-999,999
  • million - 1,000,000-999,999,999
  • billion - 1,000,000,000-999,999,999,999
  • trillion - 1,000,000,000,000+

Styling Example

With this semantic structure, styling becomes trivial using CSS attribute selectors:

/* Make cents smaller and underlined */
.fragment[type="decimal"] {
  font-size: 50%;
  text-decoration: underline;
  vertical-align: super;
}

/* Color-code by magnitude */
.fragment[subtype="hundred"] {
  background-color: #f4f4f8;
  color: #1f2d3d;
}

.fragment[subtype="thousand"] {
  background-color: #e6e6ea;
  color: #1f2d3d;
}

.fragment[subtype="million"] {
  background-color: #009fb7;
  color: white;
}

.fragment[subtype="billion"] {
  background-color: #fed766;
  color: #1f2d3d;
}

.fragment[subtype="trillion"] {
  background-color: #fe4a49;
  color: white;
}

/* Add hover effects */
.fragment[type="integer"]:hover {
  transform: scale(1.1);
  cursor: pointer;
}

/* Animate magnitude changes */
.fragment[type="integer"] {
  transition: background-color 0.3s ease;
}

The Result

$1,234,567.89 renders with:

  • Millions (1) in blue background
  • Thousands (234) in light gray background
  • Hundreds (567) in lighter gray background
  • Cents (89) smaller, underlined, superscripted

Why This Is Revolutionary

What traditional props would need:

<Money
  value={1234567.89}
  currency="USD"
  // Styling props
  centsStyle={{ fontSize: '50%', textDecoration: 'underline' }}
  hundredsStyle={{ backgroundColor: '#f4f4f8' }}
  thousandsStyle={{ backgroundColor: '#e6e6ea' }}
  millionsStyle={{ backgroundColor: '#009fb7', color: 'white' }}
  billionsStyle={{ backgroundColor: '#fed766' }}
  trillionsStyle={{ backgroundColor: '#fe4a49', color: 'white' }}
  // Event handlers
  onCentsHover={handleHover}
  onHundredsHover={handleHover}
  onThousandsHover={handleHover}
  // Animation props
  enableMagnitudeAnimations={true}
  animationDuration={300}
  // ... and it gets worse
/>

Problems:

  • Component must handle all styling logic internally
  • Can't use CSS pseudo-selectors (:hover, :focus, etc.)
  • Can't use CSS transitions/animations
  • Can't use media queries for responsive styling
  • Can't leverage CSS cascade or inheritance
  • Component bundle includes all this logic whether you use it or not

What this approach requires:

<Money currency="USD" format={MoneyGranularElementsFormatter}>
  1234567.89
</Money>

Plus a CSS file. That's it.

Implementation Simplicity

The entire formatter is ~88 lines:

const MoneyGranularElementsFormatter = ({
  components: { container: Container, number: Number },
  elements: { operator, currency, symbol },
  fragments: { number },
  negative,
  reverse,
}) => {
  // Split number by separators (. and ,)
  const granular = useMemo(() => {
    const fragments = number
      .split(/([.,])+/gi)
      .reverse()
      .reduce((stack, fragment, index) => {
        // Determine type and subtype based on position
        const attrs = getTypeFrom({ fragment, index });

        return [
          <span key={index} className="fragment" {...attrs}>
            {fragment}
          </span>,
        ].concat(stack);
      }, []);

    return <Number>{fragments}</Number>;
  }, [number]);

  const children = useMemo(() => {
    switch (true) {
      case reverse:
        return [negative && operator, granular, symbol, currency];
      default:
        return [negative && operator, symbol, currency, granular];
    }
  }, [reverse, negative, operator, currency, symbol, granular]);

  return <Container>{children}</Container>;
};

No styling logic. No prop parsing. Just semantic markup.

Real-World Use Cases

This pattern enables:

  1. Financial dashboards - Color-code by magnitude for quick scanning
  2. Accounting software - Highlight cents differently from dollars
  3. Data visualization - Animate value changes with magnitude transitions
  4. Accessibility - Add ARIA labels to each magnitude group
  5. Educational apps - Interactive tooltips explaining place values
  6. Mobile responsive - Hide certain digit groups on small screens via CSS
  7. Theming - Different color schemes per client via CSS variables

All without touching the component code.

The Key Insight

By providing semantic structure instead of style props, formatters enable use cases the component author never imagined.

The component's job is to describe what things are (decimal, thousands, millions).

The consumer's job is to decide how things look.

Perfect separation of concerns.

Third-Party Library Integration

One of the most powerful aspects of this compositional approach: effortless integration with any third-party library.

Since formatters are just functions that receive data and return JSX, you can plug in any library without modifying the core component.

Example: Number Abbreviation with Numeral.js

The MoneyRoundedFormatter demonstrates this perfectly. It integrates numeral.js for number abbreviation in just 26 lines:

import numeral from 'numeral';

const MoneyRoundedFormatter = ({
  components: { container: Container, number: Number },
  elements: { operator, symbol },
  value,
  negative,
  reverse,
}) => {
  // Use numeral.js to abbreviate the number
  const rounded = useMemo(() => numeral(value).format('0a'), [value]);
  const number = useMemo(() => <Number>{rounded}</Number>, [rounded]);

  const children = useMemo(() => {
    switch (true) {
      case reverse:
        return [negative && operator, number, symbol];
      default:
        return [negative && operator, symbol, number];
    }
  }, [reverse, negative, operator, number, symbol]);

  return <Container>{children}</Container>;
};

Usage:

<Money currency="USD" format={MoneyRoundedFormatter}>1234567</Money>
// Output: $1.2m

<Money currency="EUR" format={MoneyRoundedFormatter}>8900</Money>
// Output: €8.9k

That's it. No need to add props to the core component. No need to update the API. Just import, use, done.

Why This Matters

Traditional approach:

To add abbreviation support, you'd need to:

  1. Add the library as a dependency to the core component
  2. Add props: abbreviate={true}, abbreviationFormat="0a", abbreviationThreshold={1000}
  3. Add conditional logic inside the component
  4. Increase bundle size for all users
  5. Create tight coupling between the component and the library
  6. Risk breaking changes when updating the library

This approach:

  1. Create a new formatter file
  2. Import the library
  3. Use it

The core component knows nothing about numeral.js. The core component never changes. Users who don't need abbreviation don't pay for it.

More Integration Examples

The same pattern works with any library:

Accounting.js Integration

import accounting from 'accounting';

const MoneyAccountingFormatter = ({ value, currency, /* ... */ }) => {
  const formatted = useMemo(
    () => accounting.formatMoney(value, { symbol: currency }),
    [value, currency]
  );
  // ... compose with elements
};

Dinero.js Integration

import Dinero from 'dinero.js';

const MoneyDineroFormatter = ({ value, currency, /* ... */ }) => {
  const formatted = useMemo(
    () => Dinero({ amount: value, currency }).toFormat(),
    [value, currency]
  );
  // ... compose with elements
};

Custom Business Logic

const MoneyWithTaxFormatter = ({ value, elements, /* ... */ }) => {
  const taxRate = useTaxRate(); // Custom hook
  const withTax = useMemo(() => value * (1 + taxRate), [value, taxRate]);

  return (
    <Container>
      {elements.symbol}
      <span className="subtotal">{value}</span>
      <span className="tax">+{(value * taxRate).toFixed(2)}</span>
      <span className="total">{withTax.toFixed(2)}</span>
    </Container>
  );
};

React Spring Animation Integration

import { animated, useSpring } from 'react-spring';

const MoneyAnimatedFormatter = ({ value, elements, /* ... */ }) => {
  const props = useSpring({ number: value, from: { number: 0 } });

  return (
    <Container>
      {elements.symbol}
      <animated.span>
        {props.number.to(n => n.toFixed(2))}
      </animated.span>
    </Container>
  );
};

The Pattern

Every integration follows the same simple pattern:

  1. Import the library in your formatter
  2. Use the library's API with the data you receive
  3. Compose the result with the provided elements/components
  4. Done - no core component changes needed

Benefits

  • Zero coupling: Core component independent of any library
  • Bundle optimization: Tree-shake unused formatters and their dependencies
  • Version flexibility: Each formatter can use different library versions
  • Easy migration: Update or replace libraries without touching core code
  • Mix and match: Combine multiple libraries in one formatter
  • Future-proof: Integrate libraries that don't exist yet

Comparison Table

Aspect Traditional Props Approach Compositional Approach
Add new library Modify core component Create new formatter
Bundle impact All users pay the cost Only users of that formatter
Coupling Tight coupling Zero coupling
Version conflicts Single version for all Each formatter independent
Breaking changes Affects all users Isolated to formatter
Maintenance Core component grows Core component stable

Real-World Scenario

Imagine you need to support:

  • Standard formatting (built-in Intl)
  • Abbreviated numbers (numeral.js)
  • Accounting format (accounting.js)
  • Cryptocurrency (custom library)
  • Animated transitions (react-spring)

Traditional approach:

// Core component dependencies
import numeral from 'numeral';
import accounting from 'accounting';
import cryptoFormat from 'crypto-format';
import { useSpring } from 'react-spring';

// Component with 50+ props and complex conditional logic
const Money = ({
  value,
  currency,
  useAbbreviation,
  abbreviationFormat,
  useAccounting,
  accountingFormat,
  isCrypto,
  cryptoOptions,
  animated,
  animationDuration,
  // ... 40+ more props
}) => {
  // 200+ lines of conditional logic
};

Bundle size: ~150kb (all libraries shipped to everyone)

This approach:

// Core component - no external dependencies
const Money = ({ value, currency, format, children }) => {
  // 104 lines - pure data transformation
};

// Formatters in separate files
// MoneyRoundedFormatter.js - uses numeral.js
// MoneyAccountingFormatter.js - uses accounting.js
// MoneyCryptoFormatter.js - uses crypto-format
// MoneyAnimatedFormatter.js - uses react-spring

Bundle size: ~5kb + chosen formatters (tree-shaken)

The Key Advantage

By making formatters composable functions instead of configurable components, you get:

  • Unlimited extensibility without API growth
  • Pay-per-use bundle size via tree-shaking
  • Library ecosystem compatibility out of the box
  • Future-proof architecture for libraries that don't exist yet

You're not building a component. You're building a platform for money formatting.

Technical Highlights

Performance Optimization

Every computation is memoized with useMemo:

const fragments = useMemo(() => {
  // Parse once, reuse until currency/locale/value changes
}, [currency, locale, value]);

Locale Awareness

Built on Intl.NumberFormat - handles all locale formatting automatically:

<Money locale="pt-BR" currency="BRL">1234.56</Money>
// Output: +R$ BRL 1.234,56 (locale-aware separators)

Type Safety

PropTypes with ISO 4217 currency code validation:

Money.propTypes = {
  currency: oneOf(ISOCurrency.codes()).isRequired, // Validates against ISO 4217
  // ...
};

Component Factory Pattern

Creates bound component functions for each fragment type using higher-order functions and useCallback:

const getBoundComponentWith = useCallback(
  defaultProps => props =>
    createElement("span", { ...defaultProps, ...extraProps, ...props }),
  [extraProps]
);

This enables formatters to render fragments with custom props while maintaining defaults.

Key Learnings

This project demonstrates mastery of:

  1. React Composition Patterns - Render props, component factories, element composition
  2. API Design - Minimal surface area, maximum flexibility
  3. Separation of Concerns - Data transformation vs. presentation logic
  4. Performance - Aggressive memoization, tree-shaking
  5. Internationalization - Locale-aware formatting with Intl API
  6. Extensibility - Open/closed principle in practice
  7. Third-Party Integration - Seamless integration with any library ecosystem
  8. Problem Recognition - Identifying and solving architectural anti-patterns
  9. Technical Evolution - Awareness of modern APIs and continuous improvement opportunities

The Inspiration

After wrestling with airbnb/react-dates and its 200+ props, I realized: we're solving the wrong problem.

The question isn't "how do we expose all customization through props?"

The question is "how do we give developers the primitives to build exactly what they need?"

This component is the answer.

Running the Demo

yarn install
yarn start

Open http://localhost:3000 to see all formatters in action.

Testing

This project includes comprehensive test coverage with both unit and integration tests.

# Run unit tests (React Testing Library)
yarn test

# Run unit tests with coverage
yarn test:unit

# Run e2e tests (Playwright)
yarn test:e2e

# Run all tests
yarn test:all

Test Coverage:

  • 70+ test cases
  • Unit tests for core component and all formatters
  • Integration tests for visual rendering and cross-browser compatibility
  • See TESTING.md for detailed testing documentation

Project Structure

src/
├── components/
│   └── Money/
│       ├── Money.js              # Core component (104 lines)
│       ├── Money.style.js        # Styled-components wrapper
│       └── Formatters/
│           ├── MoneyDefaultFormatter/
│           ├── MoneyJustCurrencyFormatter/
│           ├── MoneyJustSymbolFormatter/
│           ├── MoneyJustNumbersFormatter/
│           ├── MoneyRoundedFormatter/
│           └── MoneyGranularElementsFormatter/

Tech Stack

Creating Custom Formatters

Formatters receive this shape:

{
  locale: string,           // e.g., "en-US"
  negative: boolean,        // Is value negative?
  value: number,            // Absolute value
  formatted: string,        // Full formatted string from Intl
  reverse: boolean,         // Does symbol come after number?

  fragments: {              // Raw string values
    operator: string,       // "+" or "-"
    currency: string,       // "EUR"
    symbol: string,         // "€"
    number: string,         // "1,234.56"
  },

  classNames: {             // CSS class names
    container: string,
    operator: string,
    currency: string,
    symbol: string,
    number: string,
  },

  components: {             // Bound component factories
    container: Component,
    operator: Component,
    currency: Component,
    symbol: Component,
    number: Component,
  },

  elements: {               // Pre-rendered React elements
    operator: ReactElement,
    currency: ReactElement,
    symbol: ReactElement,
    number: ReactElement,
  }
}

Example custom formatter:

const MoneyBoldSymbol = ({
  components: { container: Container },
  elements: { symbol, number },
  negative,
  reverse
}) => (
  <Container>
    {!reverse && <strong>{symbol}</strong>}
    {negative && "-"}
    {number}
    {reverse && <strong>{symbol}</strong>}
  </Container>
);

Conclusion

This isn't just a money formatting component. It's a demonstration of architectural thinking:

  • Recognizing anti-patterns (prop explosion)
  • Applying React's core philosophy (composition)
  • Designing for the pit of success (easy to extend, hard to break)
  • Prioritizing developer experience (simple, powerful, predictable)
  • Understanding when to improve implementation while preserving architecture

The result: a component that's more flexible and less complex than traditional approaches.

That's the paradox worth showcasing.

Tip

For Interviewers: The use of RegExp parsing instead of formatToParts() demonstrates an important skill—knowing when implementation details can be improved while the core architecture remains sound. The compositional pattern is the valuable insight here; the parsing mechanism is just an implementation detail that can evolve with the platform.

About

A revolutionary approach to React component design using currency formatting as an example

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors