React
1export function Selection() {2 return (3 <selectionContext.Provider value={value}>4 {children}5 </selectionContext.Provider>6 )7}
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.
In this article, we will be looking at two R3F components that are used together: Selection
and Select
.
1export type Api = {2 selected: THREE.Object3D[]3 select: React.Dispatch<React.SetStateAction<THREE.Object3D[]>>4 enabled: boolean5}6
7export const selectionContext = createContext<Api | null>(null)8
9export 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}
1export type SelectApi = JSX.IntrinsicElements['group'] & {2 enabled?: boolean3}4
5export function Select({ enabled = false, children, ...props }: SelectApi) {6 const group = useRef<THREE.Group>(null!)7 const api = useContext(selectionContext)8 useEffect(() => {9 if (api && enabled) {10 let changed = false11 const current: THREE.Object3D<THREE.Event>[] = []12 group.current.traverse((o) => {13 o.type === 'Mesh' && current.push(o)14 if (api.selected.indexOf(o) === -1) changed = true15 })16 if (changed) {17 api.select((state) => [...state, ...current])18 return () => {19 api.select((state) => state.filter((selected) => !current.includes(selected)))20 }21 }22 }23 }, [enabled, children, api])24 return (25 <group ref={group} {...props}>26 {children}27 </group>28 )29}
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
1export function App() {2 const [hovered, hover] = useState(false);3
4 return (5 <Canvas>6
7 <Selection>8
9
10 <Select11
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.
1export function App() {2 const [hovered, hover] = useState(false);3
4 return (5 <Select6 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.
Selection
to AngularLet’s bring back Selection
and dissect each part of the component.
1export type Api = {2 selected: THREE.Object3D[];3 select: React.Dispatch<React.SetStateAction<THREE.Object3D[]>>;4 enabled: boolean;5};6
7export const selectionContext = createContext<Api | null>(null);8
9export 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})7export class NgtSelection {}
Usually, the first thing that I port is the template.
React
1export 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})8export 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
1export 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})5export class NgtSelection {}
Next, we need to have a way to keep track of the selected objects.
React
1export 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})4export 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}
enabled
inputFinally, we need to expose the enabled
input from NgtSelection
to replicate the enabled
prop in Selection
React
1export 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})4export 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
1export type Api = {2 selected: THREE.Object3D[]3 select: React.Dispatch<React.SetStateAction<THREE.Object3D[]>>4 enabled: boolean5}6
7export const selectionContext = createContext<Api | null>(null)8
9export 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})5export 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}
Let’s compare how we use NgtSelection
in Angular to how we use Selection
in React
React
1export 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})10export 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})11export class SceneGraph {}
Select
to AngularLet’s look at Select
next and dissect its code.
1export type SelectApi = {2 enabled?: boolean;3} & JSX.IntrinsicElements["group"];4
5
6export 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})7export class NgtSelect {}
We’ll follow the same approach as before; starting out with the template
React
1export 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})9export 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:
ngt-group
potentially acceptsNgtSelect
into an Attribute Directive instead of a ComponentOption (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})8export 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})6export class NgtSelect {}
enabled
inputThis is similar to NgtSelection
, we’ll use Signal Input here to replicate the enabled
prop in Select
React
1export function Select({ enabled = false }) {2
3 return (4 /* ... */5 )6}
Angular
1@Directive({2 /* ... */3})4export class NgtSelect {5 enabled = input(false, { transform: booleanAttribute, alias: 'ngtSelect' });6}
group
referenceNgtSelect
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 asuseRef
in React.
React
1export 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})4export class NgtSelect {5
6 constructor() {7
8 const elementRef = inject<ElementRef<Group | Mesh>>(ElementRef);9 }10}
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
1export function Select() {2
3 const api = useContext(selectionContext);4
5 return (6 /* ... */7 )8}
Angular
1@Directive({2 /* ... */3})4export class NgtSelect {5 constructor() {6
7 const selection = inject(NgtSelection);8 }9}
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
1export 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})4export 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
1export type SelectApi = {2 enabled?: boolean;3} & JSX.IntrinsicElements["group"];4
5export 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})5export 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
1function Scene() {2 const [hovered, hover] = useState(false);3
4 return (5 <Selection>6 <Select7 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
23export function App() {24 return (25 <Canvas>26 <Scene />27 </Canvas>28 );29}
Angular
1@Component({2 standalone: true,3 template: `4 <ngt-mesh5 [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})20export 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})31export class App {32 sceneGraph = SceneGraph;33}
Let’s see NgtSelection
and NgtSelect
in action
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.