Skip to content

Commit 91626ec

Browse files
Gaic4oSolant
andcommitted
docs: code smells/cross-imports add (feature-sliced#888)
* docs: code smells/cross-imports add * fix: Title typo correction * docs: reflecting feedback * docs: reflecting feedback * Update i18n/en/docusaurus-plugin-content-docs/current/guides/issues/cross-imports.mdx Co-authored-by: Solant <runner62v6@gmail.com> --------- Co-authored-by: Solant <runner62v6@gmail.com>
1 parent 708e3b9 commit 91626ec

File tree

1 file changed

+251
-6
lines changed

1 file changed

+251
-6
lines changed

i18n/en/docusaurus-plugin-content-docs/current/guides/issues/cross-imports.mdx

Lines changed: 251 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,263 @@
11
---
22
sidebar_position: 4
3-
sidebar_class_name: sidebar-item--wip
43
pagination_next: reference/layers
54
---
65

7-
import WIP from '@site/src/shared/ui/wip/tmpl.mdx'
86

9-
# Cross-imports
7+
# Cross-import
108

11-
<WIP ticket="220" />
9+
A **cross-import** is an import **between different slices within the same layer**.
1210

13-
> Cross-imports appear when the layer or abstraction begins to take too much responsibility than it should. That is why the methodology identifies new layers that allow you to uncouple these cross-imports
11+
For example:
12+
- importing `features/product` from `features/cart`
13+
- importing `widgets/sidebar` from `widgets/header`
1414

15-
## See also
15+
Cross-imports are a code smell: a warning sign that slices are becoming coupled. In some situations they may be hard to avoid, but they should always be deliberate and either documented or shared within the team/project.
16+
17+
:::note
18+
The `shared` and `app` layers do not have the concept of a slice, so imports *within* those layers are **not** considered cross-imports.
19+
:::
20+
21+
## Why is this a code smell?
22+
23+
Cross-imports are not just a matter of style—they are generally considered a **code smell** because they blur the boundaries between domains and introduce implicit dependencies.
24+
25+
Consider a case where the `cart` slice directly depends on `product` business logic. At first glance, this might seem convenient. However, this creates several problems:
26+
27+
1. **Unclear ownership and responsibility.** When `cart` imports from `product`, it becomes unclear which slice "owns" the shared logic. If the `product` team changes their internal implementation, they might unknowingly break `cart`. This ambiguity makes it harder to reason about the codebase and assign responsibility for bugs or features.
28+
29+
2. **Reduced isolation and testability.** One of the main benefits of sliced architecture is that each slice can be developed, tested, and deployed independently. Cross-imports break this isolation—testing `cart` now requires setting up `product` as well, and changes in one slice can cause unexpected test failures in another.
30+
31+
3. **Increased cognitive load.** Working on `cart` also requires accounting for how `product` is structured and how it behaves. As cross-imports accumulate, tracing the impact of a change requires following more code across slice boundaries, and even small edits demand more context to be held in mind.
32+
33+
4. **Path to circular dependencies.** Cross-imports often start as one-way dependencies but can evolve into bidirectional ones (A imports B, B imports A). This tends to lock slices together, making dependencies harder to untangle and increasing refactoring cost over time.
34+
35+
The purpose of clear domain boundaries is to keep each slice focused and changeable within its own responsibility. When dependencies are loose, it becomes easier to predict the impact of a change and to keep review and testing scope contained. Cross-imports weaken this separation, expanding the impact of changes and increasing refactoring cost over time—this is why they are treated as a code smell worth addressing.
36+
37+
In the sections below, we outline how these issues typically appear in real projects and what strategies you can use to address them.
38+
39+
## Entities layer cross-imports
40+
41+
Cross-imports in `entities` are often caused by splitting entities too granularly. Before reaching for `@x`, consider whether the boundaries should be merged instead.
42+
Some teams use `@x` as a dedicated cross-import surface for `entities`, but it should be treated as a **last resort** — a **necessary compromise**, not a recommended approach.
43+
44+
Think of `@x` as an explicit gateway for unavoidable domain references—not a general-purpose reuse mechanism. Overuse tends to lock entity boundaries together and makes refactoring more costly over time.
45+
46+
For details about `@x`, see the [Public API documentation](/docs/reference/public-api).
47+
48+
For concrete examples of cross-references between business entities, see:
49+
- [Types guide — Business entities and their cross-references](/docs/guides/examples/types#business-entities-and-their-cross-references)
50+
- [Layers reference — Entities](/docs/reference/layers#entities)
51+
52+
## Features and widgets: Multiple strategies
53+
54+
In the `features` and `widgets` layers, it's usually more realistic to say there are **multiple strategies** for handling cross-imports, rather than declaring them **always forbidden**. This section focuses less on code and more on the **patterns** you can choose from depending on your team and product context.
55+
56+
### Strategy A: Slice merge
57+
58+
If two slices are not truly independent and they are always changed together, merge them into a single larger slice.
59+
60+
Example (before):
61+
- `features/profile`
62+
- `features/profileSettings`
63+
64+
If these keep cross-importing each other and effectively move as one unit, they are likely one feature in practice. In that case, merging into `features/profile` is often the simpler and cleaner choice.
65+
66+
### Strategy B: Push shared domain flows down into `entities` (domain-only)
67+
68+
If multiple features share a domain-level flow, move that flow into a domain slice inside `entities` (for example, `entities/session`).
69+
70+
Key principles:
71+
- `entities` contains **domain types and domain logic only**
72+
- UI remains in `features` / `widgets`
73+
- features import and use the domain logic from `entities`
74+
75+
For example, if both `features/auth` and `features/profile` need session validation, place session-related domain functions in `entities/session` and reuse them from both features.
76+
77+
For more guidance, see [Layers reference — Entities](/docs/reference/layers#entities).
78+
79+
### Strategy C: Compose from an upper layer (pages / app)
80+
81+
Instead of connecting slices within the same layer via cross-imports, compose them at a higher level (`pages` / `app`). This approach uses **Inversion of Control (IoC)** patterns—rather than slices knowing about each other, an upper layer assembles and connects them.
82+
83+
Common IoC techniques include:
84+
- **Render props (React)**: Pass components or render functions as props
85+
- **Slots (Vue)**: Use named slots to inject content from parent components
86+
- **Dependency injection**: Pass dependencies through props or context
87+
88+
#### Basic composition example (React):
89+
90+
```tsx title="features/userProfile/index.ts"
91+
export { UserProfilePanel } from './ui/UserProfilePanel';
92+
```
93+
94+
```tsx title="features/activityFeed/index.ts"
95+
export { ActivityFeed } from './ui/ActivityFeed';
96+
```
97+
98+
```tsx title="pages/UserDashboardPage.tsx"
99+
import React from 'react';
100+
import { UserProfilePanel } from '@/features/userProfile';
101+
import { ActivityFeed } from '@/features/activityFeed';
102+
103+
export function UserDashboardPage() {
104+
return (
105+
<div>
106+
<UserProfilePanel />
107+
<ActivityFeed />
108+
</div>
109+
);
110+
}
111+
```
112+
113+
With this structure, `features/userProfile` and `features/activityFeed` do not know about each other. `pages/UserDashboardPage` composes them to build the full screen.
114+
115+
#### Render props example (React):
116+
117+
When one feature needs to render content from another, use render props to invert the dependency:
118+
119+
```tsx title="features/commentList/ui/CommentList.tsx"
120+
interface CommentListProps {
121+
comments: Comment[];
122+
renderUserAvatar?: (userId: string) => React.ReactNode;
123+
}
124+
125+
export function CommentList({ comments, renderUserAvatar }: CommentListProps) {
126+
return (
127+
<ul>
128+
{comments.map(comment => (
129+
<li key={comment.id}>
130+
{renderUserAvatar?.(comment.userId)}
131+
<span>{comment.text}</span>
132+
</li>
133+
))}
134+
</ul>
135+
);
136+
}
137+
```
138+
139+
```tsx title="pages/PostPage.tsx"
140+
import { CommentList } from '@/features/commentList';
141+
import { UserAvatar } from '@/features/userProfile';
142+
143+
export function PostPage() {
144+
return (
145+
<CommentList
146+
comments={comments}
147+
renderUserAvatar={(userId) => <UserAvatar userId={userId} />}
148+
/>
149+
);
150+
}
151+
```
152+
153+
Now `CommentList` doesn't import from `userProfile`—the page injects the avatar component.
154+
155+
#### Slots example (Vue):
156+
157+
Vue's slot system provides a natural way to compose features without cross-imports:
158+
159+
```vue title="features/commentList/ui/CommentList.vue"
160+
<template>
161+
<ul>
162+
<li v-for="comment in comments" :key="comment.id">
163+
<slot name="avatar" :userId="comment.userId" />
164+
<span>{{ comment.text }}</span>
165+
</li>
166+
</ul>
167+
</template>
168+
169+
<script setup lang="ts">
170+
defineProps<{
171+
comments: Comment[];
172+
}>();
173+
</script>
174+
```
175+
176+
```vue title="pages/PostPage.vue"
177+
<template>
178+
<CommentList :comments="comments">
179+
<template #avatar="{ userId }">
180+
<UserAvatar :userId="userId" />
181+
</template>
182+
</CommentList>
183+
</template>
184+
185+
<script setup lang="ts">
186+
import { CommentList } from '@/features/commentList';
187+
import { UserAvatar } from '@/features/userProfile';
188+
</script>
189+
```
190+
191+
The `CommentList` feature remains independent of `userProfile`. The page uses slots to compose them together.
192+
193+
### Strategy D: Cross-feature reuse only via Public API
194+
195+
If the above strategies don't fit your case and cross-feature reuse is truly unavoidable, allow it only through an explicit **Public API** (for example: exported hooks or UI components). Avoid directly accessing another slice's `store`/`model` or internal implementation details.
196+
197+
Unlike strategies A-C which aim to eliminate cross-imports, this strategy accepts them while minimizing the risks through strict boundaries.
198+
199+
#### Example code:
200+
201+
```tsx title="features/auth/index.ts"
202+
203+
export { useAuth } from './model/useAuth';
204+
export { AuthButton } from './ui/AuthButton';
205+
```
206+
207+
```tsx title="features/profile/ui/ProfileMenu.tsx"
208+
209+
import React from 'react';
210+
import { useAuth, AuthButton } from '@/features/auth';
211+
212+
export function ProfileMenu() {
213+
const { user } = useAuth();
214+
215+
if (!user) {
216+
return <AuthButton />;
217+
}
218+
219+
return <div>{user.name}</div>;
220+
}
221+
```
222+
223+
For example, prevent `features/profile` from importing from paths like `features/auth/model/internal/*`. Restrict usage to only what `features/auth` explicitly exposes as its Public API.
224+
225+
## When should cross-imports be treated as a problem?
226+
227+
After reviewing these strategies, a natural question is:
228+
229+
> When is a cross-import acceptable to keep, and when should it be treated as a code smell and refactored?
230+
231+
Common warning signs:
232+
- directly depending on another slice's store/model/business logic
233+
- deep imports into another slice's internal files
234+
- **bidirectional dependencies** (A imports B, and B imports A)
235+
- changes in one slice frequently breaking another slice
236+
- flows that should be composed in `pages` / `app`, but are forced into cross-imports within the same layer
237+
238+
When you see these signals, treat the cross-import as a **code smell** and consider applying at least one of the strategies above.
239+
240+
## How strict you are is a team/project decision
241+
242+
How strictly to enforce these rules depends on the team and project.
243+
244+
For example:
245+
- In **early-stage products** with heavy experimentation, allowing some cross-imports may be a pragmatic speed trade-off.
246+
- In **long-lived or regulated systems** (for example, fintech or large-scale services), stricter boundaries often pay off in maintainability and stability.
247+
248+
Cross-imports are not an absolute prohibition here. They are dependencies that are **generally best avoided**, but sometimes used intentionally.
249+
250+
If you do introduce a cross-import:
251+
- treat it as a deliberate architectural choice
252+
- document the reasoning
253+
- revisit it periodically as the system evolves
254+
255+
Teams should align on:
256+
- what strictness level they want
257+
- how to reflect it in lint rules, code review, and documentation
258+
- when and how to reevaluate existing cross-imports over time
259+
260+
## References
16261

17262
- [(Thread) About the supposed inevitability of cross-ports](https://t.me/feature_sliced/4515)
18263
- [(Thread) About resolving cross-ports in entities](https://t.me/feature_sliced/3678)

0 commit comments

Comments
 (0)