跪拜 Guibai
← All articles
Frontend

The Model–useEffect Split That Keeps React Data Flows Sane

By 东风破_ ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Type drift between API responses and component props is a persistent source of runtime bugs in React apps. Centralizing models and isolating async side effects with useEffect gives teams a repeatable pattern that TypeScript can verify statically, cutting whole categories of stale-data and missing-field errors.

Summary

Frontend data structures get reused across state, props, and API layers. Defining them once in a shared model directory — rather than redeclaring in every file — creates a single type contract that TypeScript enforces from useState through child-component props. The article walks through a Color interface for RGB state and a MemberEntity interface for async API responses, showing how the same model constrains local interaction and remote data alike.

useEffect enters the picture when data comes from outside React. Putting a fetch call directly in the component body creates a re-render loop; wrapping it in useEffect with an empty dependency array fires the request once after mount. An immediately-invoked async function inside the effect handles the await, while the effect itself owns the timing.

The payoff is a pipeline where every handoff — Promise return type, useState generic, map callback, and child prop — references the same MemberEntity. Model answers "what shape," useEffect answers "when to load," and together they remove guesswork from async rendering.

Takeaways
Define shared data interfaces in a model/ directory so every component and API function imports the same type contract.
Use `import { type Color }` syntax to import interfaces for compile-time checking without pulling in runtime code.
Pass the same model through useState, component props, and callback signatures to keep the data shape consistent across the component tree.
Model interfaces like `Color` enforce field presence but not value ranges; components own input validation via HTML attributes or logic.
Spread existing state when updating one field of an object to avoid accidentally dropping other properties.
Declare async API functions with `Promise<Model[]>` so callers know both the async nature and the resolved shape.
Never place a fetch call directly in a component function body — it creates a re-render loop when state updates trigger new fetches.
Wrap async data loading in useEffect with an empty dependency array to run once after the component mounts.
Use an immediately-invoked async function inside useEffect to handle await, since useEffect callbacks cannot be declared async directly.
Initialize array state with an empty array so the component renders its shell immediately while data loads.
Map over state arrays with the model type annotation on the callback parameter to maintain type safety into child components.
Conclusions

Many React codebases scatter type definitions across files, then patch inconsistencies with optional chaining and defensive checks. A single model directory is a low-effort structural fix that eliminates the root cause.

The article draws a clean separation of concerns: models own structure, components own validation, and useEffect owns timing. Conflating any two of these is what produces most async-data bugs.

Using `import { type }` is underused in practice. It signals intent clearly — this import exists only for the compiler — and prevents accidental runtime references.

The pattern of spreading state before overriding a single field is simple but frequently forgotten, leading to partial state updates that break downstream components expecting complete objects.

Concepts & terms
Model directory
A dedicated folder (e.g., `src/model/`) containing TypeScript interfaces or types that describe the shape of data used across the frontend. It serves as the single source of truth for data structures shared by components, API layers, and state.
useEffect
A React hook that runs side effects — such as data fetching, timers, or subscriptions — after the component renders. It accepts a callback and a dependency array; an empty array means the effect runs once on mount.
Immediately-invoked async function in useEffect
A pattern where an async function is defined and called inside a useEffect callback: `(async () => { ... })()`. It allows `await` inside useEffect without making the effect callback itself async, which React discourages.
import { type }
TypeScript syntax that imports a type for compile-time checking only. The imported identifier is erased during compilation and produces no runtime code, making the intent explicit and avoiding accidental runtime usage.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗