cover of story

Understanding between Angular & React / Part 2: Lifecycle hooks

Understanding between Angular & React. Part 2: Lifecycle hooks

Practical examples from business cases for implementation of basic hooks of Angular in React

Maksim Dolgikh
ITNEXT
Published in
13 min readMar 29, 2024

--

Previous part: Understanding between Angular & React. Part 1: Dumb & Smart Components

Introduction

Lifecycle hooks allow you to work with a component on its full life cycle from creation to destruction.

They help to perform some logic at a certain stage of a component — updating, logging, cleaning, etc. With their help, the code becomes more concise and clear while reading. Because hooks are usually declared at the beginning of the component code, we can immediately see what logic the component performs.

Implementation of ngOnInit and ngOnDestroy hooks

The initial task

Let’s look at an example of a “Smart component” that updates over time. These updates come from an external store every second.

init-destroy lifecycle showcase

As long as the component exists, it receives updates from the store with a new value to display by connecting to it, and also when the component is destroyed it is disconnected from the store

The RxJs Observable is used as the external store (store$)

function store$(): Observable<{ value: number }> {
return timer(0, 1000).pipe(
map((v) => Math.round(Math.random() * v * 100)),
map(value => ({value})),
);
}

Angular implementation

@Component({
selector: 'ng-lifecycle-hooks-init-destroy',
standalone: true,
template: `<p>Value in store: {{ value() || 'No value' }}</p>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InitDestroyComponent implements OnInit, OnDestroy {
public value = signal<number | null>(null);
public sub: Subscription = new Subscription();

public ngOnInit(): void {
const sub = store$().subscribe(({value}) => {
this.value.set(value);
});

this.sub.add(sub);
}

public ngOnDestroy(): void {
this.sub.unsubscribe();
}
}

This example does not use the usual methods for working with Observable, for example, asyncPipe or toSignal.

Let’s take it one by one:

  • value — reactive variable (Signal) that will output the value from the store event to the template
public value = signal<number | null>(null);
  • ngOnInitis an Angular hook that is being called when the component is ready and the necessary binding of attributes on inputs and outputs is done. In this hook, we perform a subscription to the storage and set a new value to value.

The subscription itself is stored in the class property sub and passed to the Subscription instance to track and manage this subscription

public sub: Subscription = new Subscription();

public ngOnInit(): void {
const sub = store$().subscribe(({value}) => {
this.value.set(value);
});

this.sub.add(sub);
}
  • ngOnDestroy is an Angular hook which is being called when the component is destroyed.

Since the store$ is outside the component and generates values all the time, we need to unsubscribe from store$ when the component is destroyed. Failure to do so would result in a memory leak, for the subscription would still exist even when the component is destroyed.

public ngOnDestroy(): void {
this.sub.unsubscribe();
}

React implementation

export const InitDestroyHooks = () => {
const [value, setValue] = useState(0);

useLayoutEffect(() => {
const sub = store$().subscribe(({ value }) => {
setValue(value)
})

return () => sub.unsubscribe();
}, [])

return <p> Value in store: {value || 'No value'}</p>
}

React’s functional components don’t have the usual lifecycle hooks, and the component is “conditionally” re-created each time it is rendered. React provides alternative hooks to execute some logic at different stages of the component, but it’s up to us to decide when to call them

Let’s take it one by one:

  • Similar to the Angular component, we have a variable value that is used in the template and has its own update function. We use the useState hook.
const [value, setValue] = useState(0);
  • To replace the Angular hook ngOnInit (a hook that tells you when a component is created and is called before rendering), the React hook useLayoutEffect is a great option with an empty dependency array.

The empty dependency array allows you to tell React that the hook is only called once when the component is mounted.

useLayoutEffect(() => {
store$().subscribe(({ value }) => setValue(value))
}, [])
  • To replace the Angular hook ngOnDestroy (hook when destroying a component), all you need to do in the React hook useLayoutEffect is to return a Destructor function that will call the unsubscribe.

This function will be called every time the dependencies are changed, but since there are no dependencies, this function will be called only once when unmounting the component.

useLayoutEffect(() => {
const sub = store$().subscribe( //logic );

return () => sub.unsubscribe();
}, [])

Implementation of ngOnChanges hook

The initial task

Let’s say we need a component whose task is to make a request to our server by an input parameter and display the result. It is also necessary to take into account that the parameter may change and the current request will need to be cancelled

Angular implementation

@Component({
selector: 'ng-lifecycle-hooks-change',
template: `<div>Fetch result: <pre>{{ fetchResult() | json }}</pre></div>`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [JsonPipe],
})
export class ChangeComponent implements OnChanges {
@Input({ required: true }) id!: number;

private idSubject$ = new Subject<number>();

private fetchResult$ = this.idSubject$
.asObservable()
.pipe(switchMap((id) => fetch$(id)));

public fetchResult = toSignal(this.fetchResult$);

public ngOnChanges(changes: SimpleChanges): void {
if ('id' in changes) {
this.idSubject$.next(this.id);
}
}
}

function fetch$(id: number): Observable<unknown> {
return ajax
.get<unknown>(`https://jsonplaceholder.typicode.com/todos/${id}`)
.pipe(
take(1),
map((r) => r.response)
);
}

In this example only one id parameter is changed, and it is considered a better practice to use the set id(value: number){} for such cases. But if you need to create a query from multiple input variables, ngOnChanges is better

Let’s take it one by one:

  • To respond to input data, we need id parameter with the @Input() decorator that can interact with parent components. Since we want to interact with the data immediately, we set required:true flag
@Input({ required: true }) 
id!: number;
  • ngOnChanges is an Angular hook that is called the first time when properties are initially bound, and subsequent times when inputs are changed by value or reference (for objects and arrays). The moment the inputs change, Angular will pass a SimpleChanges object with the changed input properties.

For this case, we need to check that the id value has changed and passed to the idSubject$ stream

private idSubject$ = new Subject<number>();

public ngOnChanges(changes: SimpleChanges): void {
if ('id' in changes) {
this.idSubject$.next(this.id);
}
}
  • fetchResult$ is an inherited stream from idSubject$, which creates its own “fetch stream”.

The switchMap operator is used to cancel the current request when id changed. It cancels the current internal subscription and creates a new one with fetch$

private fetchResult$ = this.idSubject$
.asObservable()
.pipe(switchMap((id) => this.fetch$(id)));
  • fetch$ — RxJs-function for executing Ajax requests to API
function fetch$(id: number): Observable<unknown> {
return ajax
.get<unknown>(`https://jsonplaceholder.typicode.com/todos/${id}`)
.pipe(
take(1),
map((r) => r.response)
);
}
  • fetchResult — reactive variable to output the result of the query to the template, with automatic unsubscribing when the component is destroyed by toSignal
public fetchResult = toSignal(this.fetchResult$);

React implementation

interface OnChangeProps {
id: number;
}

const OnChange: React.FC<OnChangeProps> = ({ id }) => {
const [fetchResult, setFetchResult] = useState<string>('');

useLayoutEffect(() => {
const abortController = new AbortController();

const fetchData = async () => {
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${id}`,
{ signal: abortController.signal }
);
setFetchResult(await response.text());
} catch (error: any) {
if (error.name === 'AbortError') {
return;
}
}
};

fetchData();

return () => abortController.abort();
}, [id]);

return (
<div>
Fetch result: <pre>{fetchResult}</pre>
</div>
);
};

In this example, the query will be inside the component, but it is good practice to put the side logic in separate hooks with name use<Hook>.

The props and useState have been covered earlier, so all that remains is to parse the remaining logic:

  • To replace the Angular hook ngOnChanges (a hook that reacts to changes in input parameters before rendering) and the function in, it is enough to declare the React-hook useLayoutEffect and list the necessary parameters that should be monitored to restart the hook
useLayoutEffect(() => {
// logic here
}, [id]);
  • React doesn’t have RxJs operators for flow control (but you can use RxJs) because everything ajax-requests works on native Promise. To be able to cancel requests in React you can use AboutController.

Each time the hook is restarted from id input parameter, it calls a cleanup function () => abortController.abort() in EffectCallback and create a signal signal: abortController.signal for cancelling the new request.

useLayoutEffect(() => {
const abortController = new AbortController();

fetch(url, { signal: abortController.signal }).then(r => {
// …
});

return () => abortController.abort();
}, [id]);
  • Since useLayoutEffect accepts only synchronous functions as EffectCallback, so it is better that any asynchronous part in the hook is executed in a separate asynchronous function.
const fetchData = async () => {
// async logic
};

fetchData().then();
  • Finally the execution of the request. Due to the fact that AbortController cancels a request with an error, it is considered to be a good practice to wrap the request execution logic in try/catch to properly identify the cause of the request error
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${id}`,
{ signal: abortController.signal }
);

setFetchResult(await response.text());

} catch (error: any) {
if (error.name === 'AbortError') {
return;
}
}

Implementation of ngAfterViewInit hook

The initial task

Let’s imagine you want to create a component which, will set autofocus on a child element for some <input/> after rendering, and can also programmatically provide focus

The peculiarity of this implementation is that the child element of the input field belongs to another component, and this component should work without using the autofocus attribute, because it is used in other parts of the application where autofocus is not needed.

As a result, there are 2 situations where you need to autofocus the input element in the child component from the parent component:

  • When rendering a component
  • Manual focus on the input field by some event (button)
example for onOnChanges showcase

Angular implementation

  • Child component

First of all, let’s implement a child component with an input field and provide access to it from the code

@Component({
selector: 'ng-after-view-init-child',
standalone: true,
template: `
<div style="padding: 4px 8px; border: 1px solid #acacac">
<label for="child-input">Child input </label>
<input #inputRef type="text" id="child-input">
</div>
`
})
export class AfterViewInitChildComponent {
@ViewChild('inputRef', {read: ElementRef<HTMLInputElement>})
private inputRef: ElementRef<HTMLInputElement> | null = null;

public focus(): void {
this.inputRef?.nativeElement.focus();
}
}

Let’s take it one by one:

  • In Angular, the @ViewChild decorator is used to access HTML elements, components or directives in a component template by passing the appropriate selector.

In this case, we use inputRef selector. The read: ElementRef<HTMLInputElement> setting is additionally used so that Angular can cast the received reference to the given interface.

@ViewChild('inputRef', {read: ElementRef<HTMLInputElement>})

private inputRef: ElementRef<HTMLInputElement> | null = null;
  • The corresponding identifier of the template variable #inputRef, which is specified in the ViewChild decorator, must be added to the native element
<input #inputRef type="text" id="child-input">
  • To respect the encapsulation of the component, only the method to focus will be provided, but not the element reference itself from the component.

Since all Angular components are classes, this is easily enforced by the private and public access modifiers

private inputRef: ElementRef<HTMLInputElement> | null = null;

public focus(): void {
this.inputRef?.nativeElement.focus();
}
  • Parent component
@Component({
selector: 'ng-after-view-init-parent',
standalone: true,
template: `
<div>
<span>Parent component </span>
<button (click)="onClick()" style="margin: 4px 0">manual focus</button>
</div>

<ng-after-view-init-child #childRef></ng-after-view-init-child>
`,
imports: [AfterViewInitChildComponent],
})
export class AfterViewInitParentComponent implements AfterViewInit {
@ViewChild('childRef')
private childRef: AfterViewInitChildComponent | null = null;

public ngAfterViewInit(): void {
this.focusChild();
}

public onClick(): void {
this.focusChild()
}

private focusChild(): void {
this.childRef?.focus();
}
}

Let’s take it one by one:

  • In order to get a reference to a component it is enough to use the familiar @ViewChild decorator only by passing the “component class” instead of a template variable.

Angular will automatically figure out what kind of element it is and cast it to the AfterViewInitChildComponent type

@ViewChild(AfterViewInitChildComponent)
private childRef: AfterViewInitChildComponent | null = null;
  • In order for the parent component to autofocus the child component, you need to wait for the initial rendering of all elements and get a reference to the component in the template. The ngAfterViewInit hook can help with this.

If you try to use focusChild() in ngOnInit, it will cause an error because the component is not rendered yet, and there is no reference to the component

public ngAfterViewInit(): void {
this.focusChild();
}

private focusChild(): void {
this.childRef?.focus();
}
  • Handler function for manual focus via <button/>
// template
<button (click)="onClick()" style="margin: 4px 0">manual focus</button>

// class
public onClick(): void {
this.focusChild()
}

React implementation

In React most of the components are developed as functions, so such concepts as classes or access modifiers can not be considered, also there is no concept of template variables

  1. Child component
interface ChildHandle {
focus(): void;
}

const ChildComponent = forwardRef<ChildHandle>((_, ref) => {
const inputRef = useRef<HTMLInputElement>(null);

useImperativeHandle(ref, () => ({
focus(): void {
inputRef.current?.focus();
},
}), []);

return (
<div style={{ padding: '4px 8px', border: '1px solid #acacac' }}>
<label htmlFor="child-input">Child input </label>
<input ref={inputRef} type="text" id="child-input"></input>
</div>
);
});

Let’s take it one by one:

  • As in the Angular component, we need to get a inputRef reference to an element in the template. React uses the React-hook useRef with an initial value null for such cases. React offers a reserved element attribute ref as a binding method.
const inputRef = useRef<HTMLInputElement>(null);
// …
return (
<input ref={inputRef}></input>
);
  • Since we have a component function, the methods inside this function can be considered encapsulated and not accessible to other parent components.

To create “access” to methods inside this function we need to use the React-hook useImperativeHandle. It is where we provide public methods to handle the component from outside. This hook will bind ref reference from the parent component with methods of the current component

useImperativeHandle(ref, () => ({
focus(): void {
inputRef.current?.focus();
},
}), []);
  • A ref reference is not just a parameter, it is a separate reserved variable, but it is not available to a general React component.

For a component to accept ref, it needs to be wrapped in a forwardRef HoC-function that can provide us with this parameter for interaction. For the ref to be typed, I created the ChildHandle interface, which declares, that the right object with methods from the component will be returned to this reference

interface ChildHandle {
focus(): void;
}

const ChildComponent = forwardRef<ChildHandle>((_, ref) => {
//…
})
  • Parent component
const ParentComponent: React.FC = () => {
const childRef = useRef<ChildHandle>(null);
const focusChild = () => {
childRef.current?.focus();
}

useEffect(() => {
focusChild();
}, []);

const onClick = () => {
focusChild();
};

return (
<>
<div>
<span>Parent component </span>
<button onClick={onClick} style={{ margin: '4px 0' }}>
manual focus
</button>
</div>

<ChildComponent ref={childRef}></ChildComponent>
</>
);
};

Let’s take it one by one:

  • Similar to getting a reference to an HTML element, thanks to forwardRef, we can get a reference to a child component through the useRef hook and ref={childRef} binding
const childRef = useRef<ChildHandle>(null);

return (
// …
<ChildComponent ref={childRef}></ChildComponent>
);
  • To replace the Angular hook ngAfterViewInit (the hook is called after rendering a component and allows access to elements), we will use the React hook useEffect.

I deliberately didn’t use it in the previous examples to show the obvious difference in using from useLayoutEffect. useLayoutEffect — before rendering, useEffect — after rendering.

Similar to useLayoutEffect, I wanted the hook to work only once when the component was mounted, so I passed an empty array of dependencies to it.

const focusChild = () => {
childRef.current?.focus();
}

useEffect(() => {
focusChild();
}, []);
  • Handler function for manual focus via <button/>
  const onClick = () => {
focusChild();
};

return (
// template
<button onClick={onClick} style={{ margin: '4px 0' }}>
manual focus
</button>
);

Missed hooks

Some Angular hooks have been overlooked, but I’ll also give examples of adapting them in React components:

ngDoCheck

The hook is called after each change check cycle. It serves for its own change checks if there is a reason for it. It’s an “expensive” hook rarely used in Angular apps. Only if you are not developing your tools for other teams.

The alternative in React is the HoC-function memo

const MemoizedComponent = memo(YourComponent, comparePropsFn?)

Or useLayoutEffect with the only EffectCallback function

useLayoutEffect(() => {
// logic
})

ngAfterViewChecked

The Hook is called every time a component is rendered. It’s needed for tracking a reference to dynamic elements from a template.

The alternative in React is useEffect with passing a dependency array consisting of useRef entities.

const dynamicRef = useRef<unknown>(null);

useEffect(() => {
// AfterViewCheckedFn
}, [dynamicRef.current]);

ngAfterContentInit and ngAfterContentChecked

Hooks for working with projected content. In modern development, you’re unlikely to need to use these.

In Angular, ContentChild or ContentChildren decorators are enough to access the projected elements to reuse elements in the template.

In React, access to projected elements is ready in advance and is passed by an input prop - children

const SomeComponent: React.FC<PropsWithChildren> = ({children}) => {
return (
// template
{children}
)
}

Conclusion

The scope of this article has covered several Angular hooks and how to replace them with React hooks, specifically:

  • ngOnInit -> useEffect/useLayoutEffect with an empty dependency array
  • ngOnDestroy -> useEffect/useLayoutEffect with Destructor return
  • ngOnChanges -> useEffect/useLayoutEffect with dependency array of input props
  • ngDoCheck -> useEffect/useLayoutEffect without any dependencies
  • ngAfterViewInit -> useEffect/useLayoutEffect with an empty dependency array
  • ngAfterViewChecked -> useEffect/useLayoutEffect with dependency array of ref variables

It’s easy to see that Angular tells us — “For each lifecycle event you need to use the appropriate hook”.

React tells us— “I’ll give you ways to execute logic in component synchronously and asynchronously with the useEffect and useLayoutEffect hooks, but it’s up to you to choose when to run them

I’m well aware that I used useLayoutEffect for asynchronous operations and React-developers can argue with me, and they will be partly right.The behaviour of the hooks is very similar, and you need to understand what your goal is in order to choose the right hook

On the one hand, React looks preferable due to its minimal toolkit to cover all cases. On the other hand, it can lead to mistakes if you don’t know how to use this “flexibility”.

Chapters

Didn’t you find it strange that some of the logic is mixed up in components, and it would be better to take that logic out of the component? Maybe there are ways of separating logic into separate entities 🤔 ?

Next

Understanding between Angular & React. Part 3: Services & Context API

Reading list

Understanding between Angular & React

4 stories
main image
title image
cover of story

My content is often saved to favourites, but unfortunately, Medium’s algorithms also look at the number of claps a story has.

If my content was useful, not only save it but also give your “clap” as well. This helps promote the content

--

--