storybookjs / storybook

Storybook is a frontend workshop for building UI components and pages in isolation. Made for UI development, testing, and documentation.
https://storybook.js.org
MIT License
83.37k stars 9.11k forks source link

Angular 12 library with storybook: Cannot access 'component' before initialization #15732

Open MurhafSousli opened 2 years ago

MurhafSousli commented 2 years ago

Describe the bug

This bug only happens when setting the path in tsconfig.json to the source files of the library instead the dist file

To make an Angular library works smoothly with the app live reload, I followed this SO answer https://stackoverflow.com/a/65866136/1015648, in short, I needed to replace the default paths of the library in tsconfig.json with the source library files instead of usin the dist path.

When I added the storybook, it builds successfully but throws a runtime error:

bootstrap:27 Uncaught ReferenceError: Cannot access 'CarouselNav' before initialization
    at Object../projects/ng-gallery/src/lib/carousel/carousel-nav/carousel-nav.component.ts (carousel-nav.component.ts:26)
    at __webpack_require__ (bootstrap:24)
    at fn (hot module replacement:61)
    at Object../projects/ng-gallery/src/lib/carousel/carousel.module.ts (carousel.model.ts:37)
    at __webpack_require__ (bootstrap:24)
    at fn (hot module replacement:61)
    at Object../projects/ng-gallery/src/lib/carousel/index.ts (centralised-slider.ts:24)
    at __webpack_require__ (bootstrap:24)
    at fn (hot module replacement:61)
    at Object../projects/ng-gallery/src/lib/components/gallery.component.ts (gallery-thumbs.component.ts:15)

The error is thrown because of injecting the parent component in a directive which works fine outside the storybook

@Directive({
  selector: '[carouselNavNextButton]'
})
export class CarouselNavNextButton {
  constructor(@Inject(forwardRef(() => CarouselNav)) public parent: CarouselNav) {
  }
}
{
  "compilerOptions": {
    "paths": {
      "ng-gallery": [
        "projects/ng-gallery/src/lib"
      ],
      "ng-gallery/*": [
        "projects/ng-gallery/src/lib/*"
      ]
    }
}

To Reproduce

ng g lib my-lib

Go to projects /my-lib/src/lib directory and create index.ts file which exports lib components that are meant to be publicly available

Edit the projects /my-lib/src/public-api.ts file in a way it exports all from the previously created index.ts file, e.g.: export * from './lib/index';

{
  "compilerOptions": {
    "paths": {
      "my-lib": [
        "projects/my-lib/src/lib"
      ],
      "my-lib/*": [
        "projects/my-lib/src/lib/*"
      ]
    }
}

System

Environment Info:

  System:
    OS: macOS 11.5
    CPU: (16) x64 Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
  Binaries:
    Node: 14.16.1 - /usr/local/bin/node
    npm: 6.14.13 - /usr/local/bin/npm
  Browsers:
    Chrome: 92.0.4515.107
    Edge: 92.0.902.62
    Safari: 14.1.2
  npmPackages:
    @storybook/addon-actions: ^6.4.0-alpha.22 => 6.4.0-alpha.22 
    @storybook/addon-essentials: ^6.4.0-alpha.22 => 6.4.0-alpha.22 
    @storybook/addon-links: ^6.4.0-alpha.22 => 6.4.0-alpha.22 
    @storybook/angular: ^6.4.0-alpha.22 => 6.4.0-alpha.22 
    @storybook/builder-webpack5: ^6.4.0-alpha.22 => 6.4.0-alpha.22 
    @storybook/manager-webpack5: ^6.4.0-alpha.22 => 6.4.0-alpha.22 

Additional context

shilman commented 2 years ago

Do you a have a reproduction repo you can share? If not, can you create one? See how to create a repro. We prioritize issues with reproductions over those without. Thank you! 🙏

salmoro commented 2 years ago

I have experienced similar errors when I had circular dependencies in my code. I'd get the same runtime error as mentioned by @MurhafSousli and the stories for which those errors were thrown did not show up in the storybook dashboard. After removing the circular dependencies (usually via injection tokens) the errors were gone and the stories appeared.

The annoying part of this is that it's inconsistent with the angular build which works well in-spite of these circular dependencies.

SarcevicAntonio commented 2 years ago

I'm also stuck on this issue.

My Components work fine in Angular itself, but with Storybook I get Reference Errors.

I'll try to spin up a reproduction repo later today or tomorrow.

SarcevicAntonio commented 2 years ago

Minimal Reproduction Repo https://github.com/SarcevicAntonio/sb-ng-re

shilman commented 2 years ago

@Marklb Is this something you could take a quick look at? ☝️

Marklb commented 2 years ago

@SarcevicAntonio Thanks for the simple repro. As @salmoro said, the problem is the circular dependency and I also would most likely fix the problem with InjectionToken's.

It has been a while since I have run into this problem and I don't remember why Angular's compiler allows this, but the circular dependency is obvious in that small repro. I ran into many of those circular dependencies scattered among many files when pulling parts of my app into a library and since then I have been very careful to avoid dealing with that again. If you were to build the library with ng-packagr then you would see warnings telling you where the circular dependency is. Surprisingly, the built library seems to actually work for that one. Normally when I ignore those warnings, my apps that import the package have various errors that don't clearly point out the problem, which is why this issue isn't obvious that the circular dependency is the problem.

I forked your repro in a Stackblitz WebContainer, with one way I may fix the problem. https://stackblitz.com/edit/github-knujip?preset=node

For reference, and in case the WebContainer doesn't work, I will explain what I did.

In ChildComponent there are references to LibComponent and in LibComponent there are references to ChildComponent, which is the circular problem, because which one should be first?

First I created a new file projects/lib/src/lib/container-accessor.ts and defined an InjectionToken. I also defined an interface for the instance being injected to implement. That isn't necessary, but to avoid typing the injected instance as any or potentially hitting the circular dependency again. It also just abstracts the implementation, to where you could swap the container to anything else implementing the interface without updating your child components.

// projects/lib/src/lib/container-accessor.ts
import { InjectionToken } from '@angular/core'

/**
 * Implemented by all components that can contain components implementing `ContainerChild`.
 */
export interface ContainerAccessor {
  someFunction(): void
}

export const CONTAINER_ACCESSOR = new InjectionToken<ContainerAccessor>('ContainerAccessor')

I made LibComponent provide itself as token CONTAINER_ACCESSOR. It is still in the Injector, like it was before where it is provided as it's class, but now it is also provided by the token CONTAINER_ACCESSOR. The new token CONTAINER_ACCESSOR isn't defined by either class, so there isn't a circular dependency.

import { ContainerAccessor, CONTAINER_ACCESSOR } from './container-accessor';
...
@Component({
  selector: 'lib-parent',
  ...,
  providers: [
    { provide: CONTAINER_ACCESSOR, useExisting: forwardRef(() => LibComponent) },
  ],
})
export class LibComponent implements AfterViewInit, ContainerAccessor {
  ...
}

When providing with an InjectionToken, I don't think there is a way to just use types. So, @Inject will be used to tell the Injector which token to search for when injecting. (I also removed the redundant instance variable declaration, because adding public, private, or protected makes constructor args instance variables. Since @Optional was used, I made the type more accurate and added ?. Then readonly is just my preference, because I don't want anyone to reassign a variable that was injected.)

import { CONTAINER_ACCESSOR, ContainerAccessor } from './container-accessor';
...
export class ChildComponent implements OnInit {
  constructor(
    @Optional() @Inject(CONTAINER_ACCESSOR) private readonly parent?: ContainerAccessor
  ) { }
}

As for why Storybook's build doesn't work, but Angular's does, I would need to dig through what all is being done. Storybook merges necessary parts of Angular's Webpack config, but I don't know if it is something in Storybook's config that isn't allowing that circular dependency or something that isn't being used from Angular's config. Either way, if you don't avoid the circular dependency, you will probably run into problems later on when the project grows and Angular's builder can't figure out how to deal with the circular dependency either.

noorsilkaredia1 commented 2 years ago

This issue occurs for all the components using forwardRef. Any Class or Function used before initialization through forwardRef is handled correctly on Angular but gives an error on storybook.

I'm facing similar issue on many of my components using storybook and cannot create stories for those components which imports components using forwardRef. Consider following code for example:

@Component({
  selector: 'app-radio-group',
  template: ` <ng-content></ng-content> `,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => RadioGroupComponent),
    multi: true,
  }],
})
export class RadioGroupComponent
  implements ControlValueAccessor, OnInit, AfterContentInit {}

RadioGroupComponent is used in providers before initialization. (This is where storybook is complaining)

Importing above component or even the module where this component is declared is causing an error on storybook.

I don't think this error is caused specifically by circular dependency but simply by using anything before initialization.

PhilippeMorier commented 2 years ago

In my case I could go around Storybook's limitation (throwing error) by changing the type of the injected class to any/never.

I am using an InjectionToken, but NO forwardRef(). Furthermore, the two files import each other (#circularDependency).

I have a parent-child component:

<toggle-group>
  <toggle>Value 1<toggle>
  <toggle>Value 1<toggle>
</toggle-group>

toggle-group.directive.ts

export const TOGGLE_GROUP = new InjectionToken<ToggleGroupDirective>(
  'ToggleGroupDirective'
);

@Directive({
  selector: 'toggle-group',
  providers: [{ provide: TOGGLE_GROUP, useExisting: ToggleGroupDirective }],
})
export class ToggleGroupDirective {
  // ...
}

toggle.component.ts

@Component({
  selector: 'toggle',
  template: `
    <button>
      <ng-content></ng-content>
    </button>
  `,
})
export class ToggleComponent {
  // use property to get types right, because of `any` in constructor 🤷
  private toggleGroup: ToggleGroupDirective;

  constructor(
    // use any/never instead of `ToggleGroupDirective` to make Storybook happy
    @Inject(TOGGLE_GROUP) toggleGroup: any // 👀❗ 
  ) {
    this.toggleGroup = toggleGroup;
}
muhammedgaygisiz commented 1 year ago

I had the same issue and i changed the compiler option emitDecoratorMetadata from true to false in the storybook tsconfig.json. Thanks to John's answer here. That solved the problem in my case.

EliezerB123 commented 1 year ago

As @Marklb said, for me it was absolutely a circular dependency for me... but Angular doesn't seem to mind that I had one. More specifically:

export const LAZY_API_CONFIG = new InjectionToken<any>('example');

cannot be in module.ts, because you'll end up a. importing component.ts into the module (For @NgModule declaring the component), b. importing module.ts into the component (Because you need the InjectionToken)

Apparently Storybook gets antsy when you import two files into each other, even though Angular doesn't care or notice.

AckerApple commented 10 months ago

I had terrible circular dependencies. To find my circular dependencies and fix them, I used the following magical cli command:

npx madge --circular --extensions ts ./
TenetMax commented 4 months ago

@noorsilkaredia1 's response is right - this is very easy to reproduce with just a forward ref. This isn't limited to Angular either - the same can be reproduced with a React useRef.

What is more curious is that this only occurs for me with a deployed static build of the storybook - the story works as expected when running in dev/watch mode with npx storybook dev -p 6006

In any event, this is still occuring. Version I am seeing the issue in: "storybook": "7.6.0-alpha.3", with "@storybook/react": "^7.5.1",