Tek Bir Uygulamada 18 İş Akışı Modülü: Karmaşık Bir React Frontend'ini Yapılandırmak
DigiFlow tipik bir CRUD uygulaması değil. 18 farklı iş akışı modülüne sahip bir doküman iş akışı platformu — her birinin kendi formları, doğrulama kuralları, onay zincirleri, dashboard'ları ve iş mantığı var. Bu karmaşıklığı, sürdürülemez bir karmaşaya dönüşmeden tek bir React uygulamasında yönetmek, daha en baştan bilinçli mimari tercihler gerektirdi.
İşte binlerce kullanıcıya 18 iş akışı modülü genelinde hizmet veren bir React 19 + TypeScript 5.9 uygulamasını nasıl yapılandırdığım ve her kararı neden verdiğim.
Modül Sistemi
Her iş akışı modülü (gelen evraklar, giden evraklar, iç yazışmalar, izin talepleri vb.) esasen bir mini uygulamadır. İlk içgüdü micro-frontend'leri kullanmak olabilir, ama ekip büyüklüğümüz ve deployment modelimiz için bu fazlasıyla abartılıydı. Bunun yerine, kural tabanlı (convention-based) bir modül sistemi inşa ettim:
src/
modules/
incoming-documents/
pages/
IncomingDocumentList.tsx
IncomingDocumentDetail.tsx
IncomingDocumentCreate.tsx
components/
DocumentMetadataForm.tsx
WorkflowStepIndicator.tsx
hooks/
useIncomingDocuments.ts
useIncomingDocumentFilters.ts
services/
incomingDocumentService.ts
types/
incomingDocument.types.ts
routes.tsx
index.ts
outgoing-documents/
...aynı yapı...
leave-requests/
...aynı yapı...
Her modül aynı yapıyı izler. Yeni bir geliştirici katıldığında, deseni bir kez öğrenir. Yeni bir modül gerektiğinde, yapıyı kopyalar ve boşlukları doldurmaya başlar. Tahmin yürütme yok, mimari tartışma yok.
Route Kaydı
Her modül kendi route'larını dışa aktarır ve merkezi bir router bunları birleştirir:
typescript// modules/incoming-documents/routes.tsx
export const incomingDocumentRoutes: RouteObject[] = [
{
path: 'incoming-documents',
element: <ModuleLayout title="Incoming Documents" />,
children: [
{ index: true, element: <IncomingDocumentList /> },
{ path: 'create', element: <IncomingDocumentCreate /> },
{ path: ':id', element: <IncomingDocumentDetail /> },
{ path: ':id/edit', element: <IncomingDocumentEdit /> },
],
},
];
// router/index.tsx
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
...incomingDocumentRoutes,
...outgoingDocumentRoutes,
...leaveRequestRoutes,
...internalCorrespondenceRoutes,
// ... 14 modül daha
],
},
]);
Lazy loading, sayfa seviyesinde değil modül seviyesinde uygulanır. Her modül tek bir kod-bölünmüş (code-split) parçadır:
typescriptconst IncomingDocuments = lazy(() => import('./modules/incoming-documents'));
const OutgoingDocuments = lazy(() => import('./modules/outgoing-documents'));
Bu, her tek sayfayı lazy-load etmenin şelale (waterfall) etkisinden kaçınırken başlangıç bundle'ını küçük tutar.
State Yönetimi: Üç Katmanlı Yaklaşım
18 modülle state yönetimi hızla kaotik hale gelebilir. Bilinçli bir üç katmanlı yaklaşım kullanıyorum:
Katman 1: TanStack React Query ile Sunucu Durumu
API'den gelen tüm veriler React Query tarafından yönetilir. İstisnasız. Bu, eski veri ve loading durumlarıyla ilgili tüm bir hata kategorisini ortadan kaldırdı:
typescript// hooks/useIncomingDocuments.ts
export function useIncomingDocuments(filters: DocumentFilters) {
return useQuery({
queryKey: ['incoming-documents', filters],
queryFn: () => incomingDocumentService.getList(filters),
staleTime: 30_000, // Yeniden çekmeden önce 30 saniye
gcTime: 5 * 60 * 1000, // Çöp toplamada 5 dakika
placeholderData: keepPreviousData, // Akıcı sayfalama
});
}
Katman 2: Zustand ile Global İstemci Durumu
Modüller arası UI durumu Zustand store'larında yaşar. Buna aktif modül, sidebar durumu, kullanıcı tercihleri ve bildirim sayıları dahildir:
typescriptinterface AppStore {
sidebarCollapsed: boolean;
activeModuleId: string | null;
breadcrumbs: Breadcrumb[];
toggleSidebar: () => void;
setActiveModule: (id: string) => void;
setBreadcrumbs: (crumbs: Breadcrumb[]) => void;
}
export const useAppStore = create<AppStore>((set) => ({
sidebarCollapsed: false,
activeModuleId: null,
breadcrumbs: [],
toggleSidebar: () =>
set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
setActiveModule: (id) => set({ activeModuleId: id }),
setBreadcrumbs: (crumbs) => set({ breadcrumbs: crumbs }),
}));
Katman 3: React Context ile Yerel Bileşen Durumu
Navigasyonu atlatması gerekmeyen modüle özel durum, modüle kapsamlandırılmış React Context'te kalır:
typescript// modules/incoming-documents/context/FilterContext.tsx
const FilterContext = createContext<FilterContextType | null>(null);
export function FilterProvider({ children }: { children: ReactNode }) {
const [filters, setFilters] = useState<DocumentFilters>(defaultFilters);
const [isFilterPanelOpen, setFilterPanelOpen] = useState(false);
return (
<FilterContext.Provider value={{
filters,
setFilters,
isFilterPanelOpen,
setFilterPanelOpen,
}}>
{children}
</FilterContext.Provider>
);
}
Bu üç katmanlı ayrım, Zustand store'larının küçük kalması, React Query'nin önbelleği yönetmesi ve geçici UI durumunun global store'ları kirletmemesi anlamına gelir.
Modüller Arası Form Yönetimi
Her iş akışı modülünün formları vardır — oluşturma formları, düzenleme formları, onay formları, reddetme formları. 18 modülle form tutarlılığı kritiktir. Yup doğrulamasıyla React Hook Form üzerinde standartlaştım:
typescriptconst documentSchema = yup.object({
title: yup.string()
.required('Document title is required')
.max(500, 'Title cannot exceed 500 characters'),
departmentId: yup.number()
.required('Department is required')
.positive('Please select a valid department'),
urgencyLevel: yup.string()
.oneOf(['normal', 'urgent', 'critical'])
.required(),
attachments: yup.array()
.of(yup.mixed<File>())
.min(1, 'At least one attachment is required'),
});
function DocumentCreateForm() {
const { control, handleSubmit, formState: { errors, isSubmitting } } =
useForm({
resolver: yupResolver(documentSchema),
defaultValues: { urgencyLevel: 'normal', attachments: [] },
});
const createMutation = useMutation({
mutationFn: incomingDocumentService.create,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['incoming-documents']
});
navigate('/incoming-documents');
},
});
return (
<form onSubmit={handleSubmit((data) => createMutation.mutate(data))}>
<WFTextField name="title" control={control} label="Title" />
<WFDepartmentSelect name="departmentId" control={control} />
<WFUrgencySelect name="urgencyLevel" control={control} />
<WFFileUpload name="attachments" control={control} />
<WFSubmitButton loading={isSubmitting}>Create</WFSubmitButton>
</form>
);
}
WF önekli bileşenler, MUI 7 bileşenlerini React Hook Form entegrasyonuyla saran WFace özel bileşen kütüphanemizden gelir. Her form alanı bileşeni; form controller'dan nasıl okuyacağını, hataları nasıl göstereceğini ve MUI stillemesini nasıl ele alacağını bilir. Bu, form başına onlarca satır boilerplate'i ortadan kaldırır.
İstek Tekilleştirme (Deduplication)
API çağrıları yapan 18 modülle birlikte, yinelenen istekler gerçek bir sorundur. İki modül aynı anda aynı birim listesini isteyebilir. React Query bunu, query key tekilleştirmesi aracılığıyla doğal olarak ele alır:
typescript// Bu bileşenlerin ikisi de tek bir API çağrısını paylaşacak
function ModuleA() {
const { data } = useQuery({
queryKey: ['departments'],
queryFn: departmentService.getAll
});
// ...
}
function ModuleB() {
const { data } = useQuery({
queryKey: ['departments'],
queryFn: departmentService.getAll
});
// ...
}
React Query olmayan istekler için (dosya yüklemeleri ya da mutation yan etkileri gibi), API istemcisinde bir istek tekilleştirme katmanı inşa ettim:
typescriptconst pendingRequests = new Map<string, Promise<any>>();
async function deduplicatedGet<T>(url: string): Promise<T> {
if (pendingRequests.has(url)) {
return pendingRequests.get(url)!;
}
const promise = apiClient.get<T>(url).then(res => res.data);
pendingRequests.set(url, promise);
try {
return await promise;
} finally {
pendingRequests.delete(url);
}
}
Uluslararasılaştırma (i18n)
DigiFlow, i18next aracılığıyla Türkçe ve İngilizce'yi destekler. Anahtar çakışmalarını önlemek için her modülün kendi çeviri namespace'i vardır:
typescript// modules/incoming-documents/i18n/tr.json
{
"incomingDocuments": {
"pageTitle": "Gelen Evraklar",
"createNew": "Yeni Evrak Oluştur",
"filters": {
"dateRange": "Tarih Aralığı",
"department": "Birim"
}
}
}
Test Stratejisi
Uygulama genelinde 131 test dosyasıyla, testleri en çok önem taşıdıkları yerlere odaklıyoruz: iş mantığı ve veri dönüşümleri. UI render testleri asgari düzeyde — stil değişiklikleriyle sürekli bozulurlar ve çok az değer sağlarlar. Bunun yerine:
- Yardımcı fonksiyonlar, doğrulayıcılar ve veri dönüşümleri için birim testleri
- MSW (Mock Service Worker) ile React Query hook'ları için entegrasyon testleri
- Karmaşık etkileşimli bileşenler için bileşen testleri (iş akışı oluşturucular, çok adımlı formlar)
Temel Mimari İlkeleri
Bu kod tabanını bir yılı aşkın süredir sürdürdükten sonra, onu yönetilebilir kılan ilkeler şunlar:
- Yapılandırma yerine kural (convention over configuration). Her modül aynı görünür. Tutarlılık, kurnazlığa galip gelir.
- Üç katmanlı state. Sunucu durumu (React Query), global istemci durumu (Zustand), yerel durum (Context). Örtüşme yok.
- Modül sınırlarında lazy load. Her sayfa değil, her bileşen değil — sadece modüller.
- Paylaşılan bileşenler bir kütüphanede, modül bileşenleri yerelde kalır. İki ya da daha fazla modülde kullanılıyorsa WFace'e gider. Aksi halde modülde kalır.
- Testler, kodla aynı yapıyı izler. Her modülün, kaynak yapısını yansıtan bir
__tests__klasörü vardır.