All Blogs

Port a simple React Component to Angular

With Props Spreading, Dependency Injection via Context API, and TypeScript via TSX, React does an excellent job at providing a powerful composition model for building UIs.

On the other hand, Angular with Directive, built-in Dependency Injection, and recently Host Directive also makes a great choice for building UIs with composition in mind.

Regardless of your preferences, this article is meant to show the differences and similarities between different APIs of React and Angular. Knowing these will allow us to easily interop between the two technologies; making our knowledge richer, and the web ecosystem a better place overall.

Selection APIs

In this article, we will be looking at two R3F components that are used together: Selection and Select.

1
export type Api = {
2
selected: THREE.Object3D[]
3
select: React.Dispatch<React.SetStateAction<THREE.Object3D[]>>
4
enabled: boolean
5
}
6
7
export const selectionContext = createContext<Api | null>(null)
8
9
export function Selection({ children, enabled = true }: { enabled?: boolean; children: React.ReactNode }) {
10
const [selected, select] = useState<THREE.Object3D[]>([])
11
const value = useMemo(() => ({ selected, select, enabled }), [selected, select, enabled])
12
return <selectionContext.Provider value={value}>{children}</selectionContext.Provider>
13
}

What do Selection and Select do?

In @react-three/postprocessing, there are a few Postprocessing effects that are applied to only selected 3D objects. Selection and Select are used to track the selected objects so the effects can apply to them. Here’s a quick example with Outline

1
export function App() {
2
const [hovered, hover] = useState(false);
3
4
return (
5
<Canvas>
6
7
<Selection>
8
9
10
<Select
11
12
enabled={hovered}
13
onPointerOver={() => hover(true)}
14
onPointerOut={() => hover(false)}
15
>
16
<mesh>
17
<boxGeometry />
18
</mesh>
19
</Select>
20
21
<EffectComposer>
22
23
<Outline />
24
</EffectComposer>
25
</Selection>
26
</Canvas>
27
);
28
}

This is straightforward and follows the composition model of React pretty well. We want to enable selection, wrap the scene with Selection. We want to enable the objects to be selectable, wrap them with Select.

Though at this point, I want to point out a couple of things about Select specifically. Select is a component that renders a THREE.Group (via <group>) so it can be used to select multiple objects at once.

1
export function App() {
2
const [hovered, hover] = useState(false);
3
4
return (
5
<Select
6
enabled={hovered}
7
onPointerOver={() => hover(true)}
8
onPointerOut={() => hover(false)}
9
>
10
<mesh>
11
<boxGeometry />
12
</mesh>
13
14
<mesh>
15
<coneGeometry />
16
</mesh>
17
</Select>
18
);
19
}

However, if we want to select a single object, we still have to wrap it with Select like the original example. This feels a little awkward to me, but it’s the composition model of React.

One good thing about Select and React composition model is that it allows for Select to accept any properties that are valid for a THREE.Group via Props Spreading. This makes the Select API itself very flexible and a lot less code to write. Main example of this is the onPointerOver and onPointerOut handlers.

Well, that is enough about React for now. Let’s move on to Angular. We’ll port both Selection and Select to Angular for similar capabilities. During the process, we’ll dissect the React code to see how it works and what’s the best way to port it to Angular.

Porting Selection to Angular

Let’s bring back Selection and dissect each part of the component.

1
export type Api = {
2
selected: THREE.Object3D[];
3
select: React.Dispatch<React.SetStateAction<THREE.Object3D[]>>;
4
enabled: boolean;
5
};
6
7
export const selectionContext = createContext<Api | null>(null);
8
9
export function Selection({
10
children,
11
enabled = true,
12
}: {
13
enabled?: boolean;
14
children: React.ReactNode;
15
}) {
16
17
const [selected, select] = useState<THREE.Object3D[]>([]);
18
19
20
const value = useMemo(
21
() => ({ selected, select, enabled }),
22
[selected, select, enabled],
23
);
24
25
return (
26
27
<selectionContext.Provider value={value}>
28
{children}
29
</selectionContext.Provider>
30
);
31
}

With this in mind, we can kind of guess how Selection API is consumed. Children of Selection will access the API via useContext(selectionContext) and that is exactly how Outline consumes the Selection API.

We’ll start by stubbing out NgtSelection in Angular

1
@Component({
2
selector: 'ngt-selection',
3
standalone: true,
4
template: ``,
5
changeDetection: ChangeDetectionStrategy.OnPush,
6
})
7
export class NgtSelection {}
1. The template

Usually, the first thing that I port is the template.

React

1
export function Selection() {
2
return (
3
<selectionContext.Provider value={value}>
4
{children}
5
</selectionContext.Provider>
6
)
7
}

Angular

1
@Component({
2
/* ... */
3
template: `
4
<ng-content />
5
`,
6
/* ... */
7
})
8
export class NgtSelection {}

In this case, Selection renders its children because it needs to provide the selectionContext. For Angular, we can use ng-content. However, that is actually redundant because we already have the NgtSelection class itself as the context. So, we can change NgtSelection to a Directive instead.

React

1
export function Selection() {
2
return (
3
<selectionContext.Provider value={value}>
4
{children}
5
</selectionContext.Provider>
6
)
7
}

Angular

1
@Directive({
2
selector: '[ngtSelection]',
3
standalone: true,
4
})
5
export class NgtSelection {}
2. The value

Next, we need to have a way to keep track of the selected objects.

React

1
export function Selection() {
2
3
const [selected, select] = useState<THREE.Object3D[]>([]);
4
5
6
const value = useMemo(
7
() => ({ selected, select, enabled }),
8
[selected, select, enabled],
9
);
10
11
return (
12
/* ... */
13
)
14
}

Angular

1
@Directive({
2
/* ... */
3
})
4
export class NgtSelection {
5
6
private source = signal<THREE.Object3D[]>([]);
7
8
9
selected = this.source.asReadonly();
10
11
12
select = this.source.update.bind(this.source);
13
}
3. The enabled input

Finally, we need to expose the enabled input from NgtSelection to replicate the enabled prop in Selection

React

1
export function Selection({ enabled }) {
2
/* ... */
3
4
const value = useMemo(
5
() => ({ selected, select, enabled }),
6
[selected, select, enabled],
7
);
8
9
return (
10
/* ... */
11
)
12
}

Angular

1
@Directive({
2
/* ... */
3
})
4
export class NgtSelection {
5
6
enabled = input(
7
true,
8
{ transform: booleanAttribute, alias: 'ngtSelection'}
9
);
10
11
/* ... */
12
}

And that is it for NgtSelection. We don’t need anything from Angular to replicate the Context API because Angular has Dependency Injection built-in. The consumers can simply inject(NgtSelection) and use the public APIs. Here’s the complete code for both Selection and NgtSelection

React

1
export type Api = {
2
selected: THREE.Object3D[]
3
select: React.Dispatch<React.SetStateAction<THREE.Object3D[]>>
4
enabled: boolean
5
}
6
7
export const selectionContext = createContext<Api | null>(null)
8
9
export function Selection({ children, enabled = true }: { enabled?: boolean; children: React.ReactNode }) {
10
const [selected, select] = useState<THREE.Object3D[]>([])
11
const value = useMemo(() => ({ selected, select, enabled }), [selected, select, enabled])
12
return <selectionContext.Provider value={value}>{children}</selectionContext.Provider>
13
}

Angular

1
@Directive({
2
selector: '[ngtSelection]',
3
standalone: true,
4
})
5
export class NgtSelection {
6
enabled = input(true, { transform: booleanAttribute, alias: 'ngtSelection' });
7
private source = signal<THREE.Object3D[]>([]);
8
selected = this.source.asReadonly();
9
select = this.source.update.bind(this.source);
10
}
4. Usage

Let’s compare how we use NgtSelection in Angular to how we use Selection in React

React

1
export function Scene() {
2
return (
3
<Selection>
4
{/* children that can consume Selection */}
5
</Selection>
6
)
7
}

Angular

1
@Component({
2
standalone: true,
3
template: `
4
<ng-container ngtSelection>
5
<!-- children that can consume Selection -->
6
</ng-container>
7
`,
8
imports: [NgtSelection],
9
})
10
export class SceneGraph {}

Pretty cool right? Now, this is even cooler. In Angular, we can also use NgtSelection as a Host Directive.

1
@Component({
2
standalone: true,
3
template: `
4
<ng-container ngtSelection>
5
<!-- children that can consume Selection -->
6
</ng-container>
7
`,
8
imports: [NgtSelection],
9
hostDirectives: [NgtSelection],
10
})
11
export class SceneGraph {}

Porting Select to Angular

Let’s look at Select next and dissect its code.

1
export type SelectApi = {
2
enabled?: boolean;
3
} & JSX.IntrinsicElements["group"];
4
5
6
export function Select({ enabled = false, children, ...props }: SelectApi) {
7
8
9
const group = useRef<THREE.Group>(null!);
10
11
12
const api = useContext(selectionContext);
13
14
15
useEffect(() => {
16
/* Effect body is irrelevant here. */
17
/* All we need to know is that `group.current` and the Selection API are used here */
18
}, [enabled, children, api]);
19
20
return (
21
22
<group ref={group} {...props}>
23
{children}
24
</group>
25
);
26
}

As mentioned above, Select has both pros and cons. It is flexible and can accept any properties that are valid for a THREE.Group via Props Spreading. However, it always renders a THREE.Group even when we only want to select a single object.

Similarly, let’s stub out NgtSelect in Angular

1
@Component({
2
selector: 'ngt-select',
3
standalone: true,
4
template: ``,
5
changeDetection: ChangeDetectionStrategy.OnPush,
6
})
7
export class NgtSelect {}

We’ll follow the same approach as before; starting out with the template

1. The template

React

1
export function Select() {
2
/* ... */
3
return (
4
<group ref={group} {...props}>
5
{children}
6
</group>
7
)
8
}

Angular

1
@Component({
2
/* ... */
3
template: `
4
<ngt-group #group>
5
<ng-content />
6
</ngt-group>
7
`,
8
})
9
export class NgtSelect {}

Well, this is straightforward. But this is also where things get a little bit interesting. We need to make sure that NgtSelect can pass-through any properties that are valid for <ngt-group> from outside. In Angular, this is not possible with the Input model. Not to mention, we also need to pass-through the event handlers that are valid for <ngt-group> as well; Inputs and Outputs are separate concerns in Angular, unlike props in React.

So, how do we proceed? We have 2 options:

Option (a) is straightforward to understand but it is also a lot of code to write (and maintain). Option (b) seems complicated and sub-optimal at first. However, it allows us to delegate the responsibility of attaching NgtSelect on the consumers’ selected elements (i.e: the <ngt-group>), and in turns allows NgtSelect to NOT have to worry about passing through anything. So, we’ll go with Option (b).

Now compared to Select in React, Angular version will make the consumers do a little bit more work by using NgtSelect with ngt-group instead of making that decision for them by rendering <ngt-group>

This is actually a good thing which we will discuss in a bit.

1
@Component({
2
@Directive({
3
selector: 'ngt-select',
4
template: ``,
5
selector: '[ngtSelect]',
6
standalone: true,
7
})
8
export class NgtSelect {}

There is one more thing that we can do for NgtSelect. Since NgtSelect is now an Attribute Directive, we can control when it is instantiated by using the selector property. In this case, we only want to instantiate NgtSelect when the host element is <ngt-group> so we’ll adjust the selector to be ngt-group[ngtSelect]

Additionally, we can also make a decision that NgtSelect can be used on <ngt-mesh> as well, allowing the consumers to select single objects. Once again, we’ll adjust the selector to be ngt-group[ngtSelect], ngt-mesh[ngtSelect]

1
@Directive({
2
selector: '[ngtSelect]',
3
selector: 'ngt-group[ngtSelect], ngt-mesh[ngtSelect]',
4
standalone: true,
5
})
6
export class NgtSelect {}
2. The enabled input

This is similar to NgtSelection, we’ll use Signal Input here to replicate the enabled prop in Select

React

1
export function Select({ enabled = false }) {
2
3
return (
4
/* ... */
5
)
6
}

Angular

1
@Directive({
2
/* ... */
3
})
4
export class NgtSelect {
5
enabled = input(false, { transform: booleanAttribute, alias: 'ngtSelect' });
6
}
3. The group reference

NgtSelect is an Attribute Directive, meaning that we can inject the host element with inject(ElementRef). This is almost equivalent to useRef in Select in terms of functionality and purpose.

To be perfectly clear, I’m not saying that inject(ElementRef) is the same as useRef in React.

React

1
export function Select() {
2
3
const group = useRef<THREE.Group>(null!);
4
5
return (
6
7
<group ref={group}>
8
/* ... */
9
</group>
10
)
11
}

Angular

1
@Directive({
2
/* ... */
3
})
4
export class NgtSelect {
5
6
constructor() {
7
8
const elementRef = inject<ElementRef<Group | Mesh>>(ElementRef);
9
}
10
}
4. Consuming Selection API

NgtSelect is going to be supposed to be used within NgtSelection so natually, we’ll have access to NgtSelection via inject(NgtSelection) in NgtSelect

React

1
export function Select() {
2
3
const api = useContext(selectionContext);
4
5
return (
6
/* ... */
7
)
8
}

Angular

1
@Directive({
2
/* ... */
3
})
4
export class NgtSelect {
5
constructor() {
6
7
const selection = inject(NgtSelection);
8
}
9
}
5. The effect

I mentioned earlier that the effect body is irrelevant here. So we’ll just point out which API from Angular that we can utilize to execute the same side-effects as Select in React.

React

1
export function Select() {
2
3
useEffect(() => {
4
/* operate on THREE.Group reference and Selection context here */
5
}, [/* ... */]);
6
7
return (
8
/* ... */
9
)
10
}

Angular

1
@Directive({
2
/* ... */
3
})
4
export class NgtSelect {
5
constructor() {
6
7
const autoEffect = injectAutoEffect();
8
9
10
afterNextRender(() => {
11
12
13
autoEffect(() => {
14
/* operate on host element and NgtSelection here */
15
/* since our host element is either a Group or Mesh, we can have conditional code paths here */
16
})
17
})
18
}
19
}

And that is it for NgtSelect. Let’s see both versions side-by-side

React

1
export type SelectApi = {
2
enabled?: boolean;
3
} & JSX.IntrinsicElements["group"];
4
5
export function Select({ enabled = false, children, ...props }: SelectApi) {
6
const group = useRef<THREE.Group>(null!);
7
const api = useContext(selectionContext);
8
9
useEffect(() => {
10
/* Effect body is irrelevant here. */
11
/* All we need to know is that `group.current` and the Selection API are used here */
12
}, [enabled, children, api]);
13
14
return (
15
<group ref={group} {...props}>
16
{children}
17
</group>
18
);
19
}

Angular

1
@Directive({
2
selector: 'ngt-group[ngtSelect], ngt-mesh[ngtSelect]',
3
standalone: true,
4
})
5
export class NgtSelect {
6
enabled = input(false, { transform: booleanAttribute, alias: 'ngtSelect' });
7
8
constructor() {
9
const elementRef = inject<ElementRef<Group | Mesh>>(ElementRef);
10
const selection = inject(NgtSelection);
11
const autoEffect = injectAutoEffect();
12
13
afterNextRender(() => {
14
autoEffect(() => {
15
/* operate on host element and NgtSelection here */
16
/* since our host element is either a Group or Mesh, we can have conditional code paths here */
17
})
18
})
19
}
20
}

Now that we have NgtSelection and NgtSelect ported, let’s compare their usages between React and Angular

React

1
function Scene() {
2
const [hovered, hover] = useState(false);
3
4
return (
5
<Selection>
6
<Select
7
enabled={hovered}
8
onPointerOver={() => hover(true)}
9
onPointerOut={() => hover(false)}
10
>
11
<mesh>
12
<boxGeometry />
13
</mesh>
14
</Select>
15
16
<EffectComposer>
17
<Outline />
18
</EffectComposer>
19
</Selection>
20
)
21
}
22
23
export function App() {
24
return (
25
<Canvas>
26
<Scene />
27
</Canvas>
28
);
29
}

Angular

1
@Component({
2
standalone: true,
3
template: `
4
<ngt-mesh
5
[ngtSelect]="hovered()"
6
(pointerover)="hovered.set(true)"
7
(pointerout)="hovered.set(false)"
8
>
9
<ngt-box-geometry />
10
</ngt-mesh>
11
12
<ngtp-effect-composer>
13
<ngtp-outline />
14
</ngtp-effect-composer>
15
`,
16
schemas: [CUSTOM_ELEMENTS_SCHEMA],
17
hostDirectives: [NgtSelection],
18
imports: [NgtpEffectComposer, NgtpOutline, NgtSelect]
19
})
20
export class SceneGraph {
21
hovered = signal(false);
22
}
23
24
@Component({
25
standalone: true,
26
template: `
27
<ngt-canvas [sceneGraph]="sceneGraph" />
28
`,
29
imports: [NgtCanvas]
30
})
31
export class App {
32
sceneGraph = SceneGraph;
33
}

Let’s see NgtSelection and NgtSelect in action

Conclusion

In this article, we’ve explored the differences between React and Angular in terms of how they handle composition. Angular, while being a more opinionated, still provides a lot of flexibility for composition. The difficult parts are to know what’s available and the trade-offs of each approach. I hope that you learned something new and have fun learning it. See y’all in the next blog post.

Published on Tue Aug 06 2024


Angular React