DEV Community

George Koval.
George Koval.

Posted on • Updated on

The right way to organise imports in Angular app (w/o cyclic deps)

You're probably familiar with imports like that

 import { UsersListComponent } from '../../../ui/users-list/users-list.component.ts
Enter fullscreen mode Exit fullscreen mode

And that doesn't look great (actually, the issue with code like that not only about being "beautiful", but also about avoiding (~lowering the amount of) merge requests conflicts during refactoring . If you need to implement a big refactoring, implying relocation of files and directories, then usage of relative paths is going to greatly disservice you, creating pre-conditions for merge requests' conflicts). What can be done about it?

We could try to add aliases in tsconfig.json like this

 {
   "paths": {
      "@app/ui": ["src/app/my-module/ui"]
    }
 }
Enter fullscreen mode Exit fullscreen mode

and combine it with index.ts files (also called "barrels") in the users-list and ui folders
// inside of /users-list folder create an index.ts file and write the following:

  export { UsersListComponent } from './users-list.component.ts';
Enter fullscreen mode Exit fullscreen mode

Yes, that way an import could be turned into this

  import { UsersListComponent } from '@app/ui';
Enter fullscreen mode Exit fullscreen mode

About barrel files it's important to mention that you should not export them in implicit way, using the asterisk sign (*).

 export * from './';
Enter fullscreen mode Exit fullscreen mode

Always prefer explicit export, because implicit export is one of the most prominent factors for circular dependency error (you don't know what exactly you're exporting, especially if there's a big directory, it's easy to miss cross dependency).

In case you have not one, but multiple components, you still can use the same notion, but you have to explicitly specify all of the components, for instance:
// components/index.ts

  export { UsersListComponent } from './users-list/users-list.component.ts';
  export { UserProfileComponent } from './user-profile/user-profile.component.ts
  ... 
Enter fullscreen mode Exit fullscreen mode

With setup like that you can use the following imports:

 import {
  UsersListComponent,
  UserProfileFormComponent,
  UserProfileRouteComponent,
  UserSettingsFormComponent,
  UserAvatarComponent, 
  ...
 } from '@app/ui';
Enter fullscreen mode Exit fullscreen mode

When c.-d. errors can happen? If you export (using barrels) something with external dependencies. Image you have a folder where there's several different files, like this

 folder/region.enum.ts
 folder/customer-type.enum.ts
 folder/customer.interface.ts
 folder/customer.dto.ts
 folder/customer.model.ts
Enter fullscreen mode Exit fullscreen mode

If these entities depend only on themselves (for instance, customer interface may import region.enum and customer-type.enum), it's quite safe to export them using barrel file, like this:
folder/index.ts

 export { Region } from './region.enum.ts';
 export { CustomerType } from './customer-type.enum.ts';
 export { CustomerDTO } from './customer.dto.ts';
 export { ICustomer } from './customer.interface.ts';  
 export { Customer } from './customer.model.ts';   
Enter fullscreen mode Exit fullscreen mode

Situations that potentially could lead to c.-d. errors can happen if those files have external dependencies or you export them using "asterisk" symbol (*), like this "export * from './'.
So, two main recommendations to avoid c.-d. errors:

  1. Do not use asterisk sign ( implicit export ).
  2. Be careful with external dependencies (outside of current folder). If your folder has entities with external dependencies (for example components folder), it doesn't mean this feature should not be used, you just have to be careful (you get the most problems from implicit export). You still can get the error (you just need to import one entity inside another and otherwise), but in most cases they occur when you have several cross-dependable modules. So, if you have entities in a folder, depending only on each other and you do not use asterisk notation , you're safe (using explicit export is the most important part. Again, you can use barrels for components folder if explicitly exported).

I've worked once with nestJS application and was trying to implement barrels there. I was faced with circular dependency error not easy to explain. After reading issues on github I've found out what was the reason behind. If you have a folder ("services") and one entity from that folder imports (= depends on) another entity (another service), than you should NOT use barrel notation here

import { AuthService } from "@app/services"; 
Enter fullscreen mode Exit fullscreen mode

Instead we should import it the old way:

import { AuthService } from "./auth.service";
Enter fullscreen mode Exit fullscreen mode

(there's more complicated solution that was proposed in one of the issues, implying import rearrangement in index.ts files in such a way that it reflects the structure of dependent relations between them, but I don't think it's a practical solution, especially if you have an huge application).
So, general rule here is if you have multiple entities in a folder with barrel, and there're dependent relations between them, these entities should be imported without barrel notation.

But sometimes you have quite complicated hierarchy, so you cannot use general name "@app/ui" and need something more specific, like "@app/ui/users". There's a way to do it.

Change your tsconfig into this:

 {
   ...
   "@app/ui/*" : "src/app/my-module/ui/*",
   ...
 }
Enter fullscreen mode Exit fullscreen mode

And then import your component like this

 import { UsersListComponent } from '@app/ui/users-list';
Enter fullscreen mode Exit fullscreen mode

by using index.ts file in the users-list folder we don't have to duplicate the same name ('users-list/users-list.component'), and by using the asterisk sign here (it's ok here, but not in the export) & aliases we're able to specify a particular folder avoiding circular dependency error.
For instance, that's how import would look like for other components from the same module:

import { 
  UserComponent,
  UserProfileFormComponent,
  UserType, 
  IUser, 
  UserDTO
} from '@app/ui/users';
import { 
  AvatarComponent,
  IAvatar
} from '@app/ui/avatar';
... 
Enter fullscreen mode Exit fullscreen mode

One of the most obvious ways to utilize "barrels" (index.ts) files are entities like "util" functions, enums, interfaces, DTOs and so on.

import { Region, Selection, Brands } from "@app/domain/enums"   
Enter fullscreen mode Exit fullscreen mode

Final Notation here. Barrels once were present in angular guidelines and then they were removed from it. As I understand it, removal doesn't mean barrels are forbidden or discouraged, they were removed because it's not appropriate place for them (it's not an angular feature). If you look into angular source code, you can easily find heavy usage of barrels, for example just a random angular source code:

https://github.com/angular/angular/blob/master/packages/core/src/di/index.ts 
Enter fullscreen mode Exit fullscreen mode

You can also search for "export { } from" in angular repo and find more than 200 references. So it's a feature heavily used inside of the framework.
One just shouldn't use it with asterisk sign ("implicit export") and it's gonna be fine.

Also, absolute paths can be used for styles. Suppose you have a mixins.scss file in src/styles folder. Right now you import them like this

@import "../../../../../mixins.scss";
Enter fullscreen mode Exit fullscreen mode

And you would like to have smth like this

 @import "mixins";
Enter fullscreen mode Exit fullscreen mode

That's quite easy to do. In angular.json, architect > builder > options add a new field called

"stylePreprocessorOptions": {
  "includePaths": [ "src/styles" ]
}
Enter fullscreen mode Exit fullscreen mode

Here you're specifying a folder , not a particular file. If you have multiple different entities in that folder, than all of them would be automatically recognised for import. No more relative paths, and in case the path is changed, you'd have to update it only in one place.

Discussion (0)