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
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.
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
ortoSignal
.
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);
ngOnInit
is 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 tovalue
.
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 theuseState
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 hookuseLayoutEffect
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 hookuseLayoutEffect
is to return aDestructor
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 theset 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 setrequired: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 aSimpleChanges
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 fromidSubject$
, 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 bytoSignal
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-hookuseLayoutEffect
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 useRxJs
) because everything ajax-requests works on nativePromise
. To be able to cancel requests in React you can useAboutController
.
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 asEffectCallback
, 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 intry/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)
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 theViewChild
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 atemplate 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()
inngOnInit
, 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
- 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-hookuseRef
with an initial valuenull
for such cases. React offers a reserved element attributeref
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 theuseRef
hook andref={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 hookuseEffect
.
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 arrayngOnDestroy
->useEffect/useLayoutEffect
withDestructor
returnngOnChanges
->useEffect/useLayoutEffect
with dependency array of input propsngDoCheck
->useEffect/useLayoutEffect
without any dependenciesngAfterViewInit
->useEffect/useLayoutEffect
with an empty dependency arrayngAfterViewChecked
->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
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