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 asuseRefin 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.