How to Build a Scalable Frontend Architecture for Agile React Projects
This article explores the challenges of agile front‑end development, presents a clean architecture based on DDD, hexagonal and layered principles, and demonstrates practical React implementations—including account display, coin‑fly animation, withdrawal dialogs, and task components—to reduce coupling and improve maintainability.
Background
The author reflects on recurring doubts during daily and promotional agile development, aiming to clarify core problems, propose solutions, and share design ideas for front‑end projects.
Key Questions
Why do combined logic and visual changes cost so much in agile iterations?
What aspects of agile development are mutable and immutable?
Should development focus on view‑oriented or data‑oriented approaches?
Example Implementation
A simple business case—displaying account information—illustrates the solution.
Account Component
import { useEffect, useState } from 'react';
import styles from './index.module.less';
const Account = () => {
const [account, setAccount] = useState(0);
useEffect(() => {
setTimeout(() => setAccount(12.34), 1000);
}, []);
return (
<div className={styles.stickyAccountWrap}>
<div className={styles.stickyAccount}>
<div className={styles.stickyAccountGoldPocketPic} />
<div className={styles.stickyAccountTitleContainer}>
<div className={styles.stickyAccountTitle}>
<div>{account}</div>
<div className={styles.unit}>元</div>
</div>
</div>
<div className={styles.withdraw} />
</div>
</div>
);
};
export default Account;Coin‑Fly Animation
import { CSSProperties, FC, useRef, useEffect, useCallback } from 'react';
import anime from 'animejs';
import styles from './index.module.less';
interface ICoinsFly { style?: CSSProperties; onEnd: () => void; }
const CoinsFly: FC<ICoinsFly> = ({ style, onEnd }) => {
const wrapRef = useRef<HTMLDivElement>(null);
const rpx2px = useCallback((rpx) => (rpx / 750) * window.screen.width, []);
useEffect(() => {
anime({
targets: wrapRef.current?.childNodes,
delay: anime.stagger(90),
translateY: [{ value: 0 }, { value: -rpx2px(334), easing: 'linear' }],
translateX: [{ value: 0 }, { value: -rpx2px(98), easing: 'cubicBezier(.05,.9,.8,1.5)' }],
scale: [{ value: 1 }, { value: 0.5, easing: 'linear' }],
opacity: [{ value: 1 }, { value: 0, easing: 'cubicBezier(1,0,1,0)' }],
duration: 900,
complete: onEnd,
});
}, []);
return (
<div className={styles.container} style={style} ref={wrapRef}>
{[...Array(8)].map((_, i) => (
<div key={i} className={styles.coin} />
))}
</div>
);
};
export default CoinsFly;Withdrawal Dialog
import styles from './index.module.less';
export interface DialogData { a: string; b: string; c: string; d: string; e: string; }
interface IProps { data: DialogData; onClose?: () => void; }
const WithdrawDialog = ({ data, onClose }: IProps) => {
const { a, b, c, d } = data;
return (
<div className={styles.popup}>
<div className={styles.content}>
<div className={styles.header}>
<div className={styles.icon} />
<div className={styles.title}>{a}</div>
</div>
<div className={styles.body}>
<div className={styles.amountCon}>
<div className={styles.amount}>{b}</div>
<div className={styles.unit}>元</div>
</div>
<div className={styles.dividing} />
<div className={styles.userContent}>
<div className={styles.userItem}>
<div className={styles.title}>提现账户</div>
<div className={styles.userText}>{c}</div>
</div>
<div className={styles.userItem}>
<div className={styles.title}>打款方式</div>
<div className={styles.userText}>{d}</div>
</div>
</div>
<div className={styles.btn} onClick={onClose}>开心收下</div>
</div>
</div>
</div>
);
};
export default WithdrawDialog;Task Component
import { useState, useEffect, useCallback } from 'react';
import styles from './index.module.less';
enum TASK_STATUS { PROGRESS = 'progress', COMPLETE = 'complete' }
const TASK_INFO_MAP = {
[TASK_STATUS.PROGRESS]: { btn: '进行中' },
[TASK_STATUS.COMPLETE]: { btn: '已完成' },
};
const Task = ({ setDialogData, setShowDialog }) => {
const [state, setState] = useState(TASK_STATUS.PROGRESS);
useEffect(() => {
setTimeout(() => {
alert('完成任务');
setState(TASK_STATUS.COMPLETE);
}, 3000);
}, []);
const btnCallback = useCallback(() => {
if (state === TASK_STATUS.COMPLETE) {
setDialogData({ a: '3000', b: '123456789123456789', c: '支付宝打款', d: '提现成功,预计2小时到账', e: '0.3' });
setShowDialog(true);
}
}, [state]);
return (
<div className={styles.taskWrap}>
<div className={styles.taskImg} />
<div className={styles.taskDesc}>
<div className={styles.action}>完成任务节即可提现</div>
<div className={styles.detailText}>完成后可提现 0.6 元</div>
</div>
<div className={styles.taskBtn} onClick={btnCallback}>{TASK_INFO_MAP[state].btn}</div>
</div>
);
};
export default Task;Design Philosophy
The article advocates a clean, layered architecture where inner layers contain abstract domain models and business rules, while outer layers handle UI and framework concerns. Data flows inward‑outward, keeping view components stateless consumers of business state managed by hooks or global stores.
Practical Workflow
Define business domain models (e.g., account, pop) using a state library.
Implement server modules that simulate API calls and update models.
Create application services that orchestrate server calls and expose init or refresh methods.
Build UI components that only consume model state and trigger application services.
Conclusion
By separating concerns through domain‑driven design and clean layering, the front‑end code becomes low‑coupled, highly maintainable, and adaptable to rapid agile iterations without costly refactoring.
Alibaba Cloud Developer
Alibaba's official tech channel, featuring all of its technology innovations.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
