Original Article Here
The drawing was made via Excalidraw.
In this article, I'm going to show you how to mimic React Context API in Angular, I'll start by defining React Context, talk about what problem is meant to solve, and a possible implementation in Angular.
I will focus more on implementation and detail it as possible rather than explaining definitions, nonetheless, I'll make sure to explain any irrelevant terms.
If at any point you don't feel interested in reading further, think of this article as a new approach for component communication in Angular.
For clarity about what I’m going to talk about, the project is available to browse through Github. or a Demo if you prefer.
From React Documentation
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
props in Angular terms corresponds to Inputs
In other words, context can help you to pass down inputs/props through a components tree without the need to define them at every level/component.
Words 📝 might not be that efficient, a practical example might be.
Here's 4 components (AppComponent, Parent, Child, Grandchild), the AppComponent passes a value to the Parent component, the Parent component will pass it to the Child component which forwards it to the Grandchild component.
@Component({
selector: 'app-root',
template: '<app-parent [familyName]="familyNameValue"></app-parent>'
})
export class AppComponent {
familyNameValue = 'The Angulars';
}
@Component({
selector: 'app-parent',
template: '<app-child [familyName]="familyName"></app-child>'
})
export class ParentComponent {
@Input() familyName: string;
}
@Component({
selector: 'app-child',
template: '<app-grandchild [familyName]="familyName"></app-grandchild>'
})
export class ChildComponent {
@Input() familyName: string;
}
@Component({
selector: 'app-grandchild',
template: 'Family Name: {{familyName}}'
})
export class GrandchildComponent {
@Input() familyName: string;
}
As you see, we had to declare the same input at every component starting from the Parent down the Grandchild, in React terms this is called Prop Drilling.
Going to the definition again
Context provides a way to pass data through the component tree without having to pass
propsInputs down manually at every level.
Good, let's see the Context way.
Hint: I'll explain the implementation later on. keep reading for now.
What if you can remove the inputs and only have a generic one that could be accessed from anywhere in the tree, like this
@Component({
selector: 'app-root',
template: `
<context name="FamilyContext">
<provider name="FamilyContext" [value]="familyNameValue"> // This part
<app-grandchild> </app-grandchild>
</provider>
</context>
`
})
export class AppComponent { }
And for the component that needs the value
@Component({
selector: 'app-grandchild',
template: `
<consumer name="FamilyContext">
<ng-template let-value>
Family Name: {{value}}
</ng-template>
</consumer>
`
})
export class GrandchildComponent { }
While this approach seems to work, I do not think a lot of people will agree on this, I myself thought about sandboxing first, maybe that's why there's no like to React Context API in Angular. but again see it as a different way to achieve the same result.
By now it's clear what problem does Context API solves. It's time to see how it works.
Warning: I'll use React components 😏.
Context API comes with two important components, Provider and Consumer. Provider is the component that will pass a value for decedents consuming components. One provider can have multiple consumers and other providers.
Consumer, as you may have thought, will consume Provider value. React will go up the component tree starting from the Consumer component to find the nearest Provider and provide its value to that Consumer as callback style, if none is found a default value will be used instead. The Consumer will re-render whenever a Provider ancestor value changes.
To create context you simply call createContext
passing default value if needed, a context object with Provider and Consumer components attached to it will return.
const MyContext = React.createContext('defaultValue');
The provider have value
props that will passed down to the consumers.
function App() {
return (
<MyContext.Provider value="valueToBeConsumedByDescendantsConsumer">
<ComponentThatHaveConsumerAsChild />
</MyContext.Provider>
);
}
The consumer takes a function with the Provider value as an argument, the function will be called (re-render 🙃) whenever the Provider value changes.
function ComponentThatHaveConsumerAsChild() {
return (
<MyContext.Consumer>
{(value) => (<h1>{value}</h1>)}
</MyContext.Consumer>
);
}
You might want to know that this is not the only way to consume context, there's contextType
and useContext
, I won't cover them because those are applicable only to React way of doing things.
if you didn't get the whole picture, check the official docs, perhaps it would be more helpful.
Enough talking about React. It's time to code.
In Angular things are different, so we'll do things in different styles but the same concept and goals remain.
If you start this article from the beginning you saw that we introduced three components
context
provider
consumer
and ended up using them like this
@Component({
selector: 'app-root',
template: `
<context name="FamilyContext"> // (1) -----> The Context Component
<provider name="FamilyContext" [value]="familyNameValue"> // (2) -----> The Provider Component
<app-parent> </app-parent>
</provider>
</context>
`
})
export class AppComponent { }
@Component({
selector: 'app-grandchild',
template: `
<consumer name="FamilyContext"> // (3) -----> The Consumer Component
<ng-template let-value>
Family Name: {{value}}
</ng-template>
</consumer>
`
})
export class GrandchildComponent { }
I'll explain each component in detail soon.
export function assertNotNullOrUndefined<T>(value: T, debugLabel: string): asserts value is NonNullable<T> {
if (value === null || value === undefined) {
throw new Error(`${ debugLabel } is undefined or null.`);
}
}
export function assertStringIsNotEmpty(value: any, debugLabel: string): asserts value is string {
if (typeof value !== 'string') {
throw new Error(`${ debugLabel } is not string`);
}
if (value.trim() === '') {
throw new Error(`${ debugLabel } cannot be empty`);
}
}
This component is responsible for declaring a scope for providers and consumers, providers can only be under their context, the same rule applies to consumers.
Unlike React Context API, we don't have reference to a context so in order to ensure the relationship between providers and consumers to a context we need to give the context and its components a name.
A name makes it possible to
Another thing related to the context component is the defaultValue
, if you recall from above if a context doesn't have any provider a default value will be used instead.
In the previous Image, Consumer ( A ) will have the value of the Context because there's no provider above it, and Consumer ( B ) will have the value of Provider ( 1 ).
Initial Implementation
@Component({
selector: 'context',
template: '<ng-content></ng-content>' // ----> (1)
})
export class ContextComponent implements OnInit, OnChanges {
@Input() name!: string; // ----> (2)
@Input() defaultValue?: any; // ----> (3)
constructor() { }
ngOnInit(): void {
assertStringIsNotEmpty(this.name, 'Context name'); // ----> (4)
}
ngOnChanges(changes: SimpleChanges): void {
const nameChange = changes.name;
if (nameChange && !nameChange.isFirstChange()) {
const { currentValue, previousValue } = nameChange;
throw new Error(`Context name can be initialized only once.\n Original name ${ previousValue }\n New name ${ currentValue }`);
}
}
}
value
that will be provided to the consuming components in case there's no provider for this context.This component will pass down its value to the consumers hence we need to have an input for that value. Also, you can have zero or more provider components for the same context. consumers will get the value from the nearest one.
In the previous Image, Consumer ( A ) will have the value of the Context, but Consumer ( B ), Consumer ( C ), and Consumer ( E ) will have the value of Provider ( 1 ). Consumer ( D ) will have the value of Provider ( 2 ) because it is the nearest one.
Initial Implementation
@Component({
selector: 'provider',
template: '<ng-content></ng-content>'
})
export class ProviderComponent implements OnInit {
@Input() name!: string; // ----> (1)
@Input() value?: any; // ----> (2)
ngOnInit(): void {
assertStringIsNotEmpty(this.name, 'Provider context name');
if (this.value === undefined) { // ----> (3)
throw new Error(`Provider without value is worthless.`);
}
}
ngOnChanges(changes: SimpleChanges): void {
const nameChange = changes.name;
if (nameChange && !nameChange.isFirstChange()) {
const { currentValue, previousValue } = nameChange;
throw new Error(`Context name can be initialized only once.\n Original name ${ previousValue }\n New name ${ currentValue }`);
}
}
}
value
that will be provided to the consuming components.The component will eventually have the value of the nearest provider or the default context value in case no provider is found up in the tree.
before digging into it, let's see the example usage first.
@Component({
selector: 'app-grandchild',
template: `
<consumer name="FamilyContext">
<ng-template let-value>
Family Name: {{value}}
</ng-template>
</consumer>
`
})
export class GrandchildComponent { }
ng-template
will be used as a convenient way to be able to provide the nearest provider value
or the context defaultValue
using template variable let-value
and to have more control over the change detection process. More about this later on.
Initial Implementation
@Component({
selector: 'consumer',
template: '<ng-content></ng-content>',
})
export class ConsumerComponent implements OnInit {
@Input() name!: string; // ----> (1)
@ContentChild(TemplateRef, { static: true }) templateRef!: TemplateRef<any>; // ----> (2)
ngOnInit(): void {
assertStringIsNotEmpty(this.name, 'Consumer context name');
if (this.templateRef === undefined) { // ----> (3)
throw new Error(`
Cannot find <ng-template>, you may forget to put the content in <ng-template>.
If you do not want to put the content in context then no point in using it.
`);
}
}
ngOnChanges(changes: SimpleChanges): void {
const nameChange = changes.name;
if (nameChange && !nameChange.isFirstChange()) {
const { currentValue, previousValue } = nameChange;
throw new Error(`Context name can be initialized only once.\n Original name ${ previousValue }\n New name ${ currentValue }`);
}
}
}
static: true
used to be able to get it in ngOnInit
.ng-template
is mandatory. why would you need to use the consumer if you're not making use of it is value?RECAP: all the code right now only validates the inputs.
The next step is to make sure providers and consumers components are using the correct context.
Hopefully, You know Dependency Injection and how the resolution process works. in nutshell, You inject a dependency and Angular will search for the implementation in several injectors if none is found an error will be all over the browser console 😁.
It is important to understand the resolution process in order to understand the rest of the code. the validation and value resolving logic relying on that mechanism. basically, we'll link each component type with the immediate next one above it, it is like creating a chain of components each has its parent and the final one (first on the tree) will have null. just like the Prototype Chain 😁. take a look at the next image, perhaps it will clear the idea.
First, adding a method to ContextComponent
that will ensure no other context with the same name is exist.
@Component({
selector: 'context',
template: '<ng-content></ng-content>',
})
export class ContextComponent implements OnInit {
@Input() defaultValue?: any;
@Input() name!: string;
constructor(
@Optional() @SkipSelf() public parentContext: ContextComponent | null // ----> (1)
) { }
ngOnInit(): void {
assertStringIsNotEmpty(this.name, 'Context name');
this.ensureContextUniqueness(this.name); // ----> (2)
}
... code omitted for brevity
public getContext(contextName: string) { // ----> (3)
let context: ContextComponent | null = this;
while (context !== null) {
if (context.name === contextName) {
return context;
}
context = context.parentContext;
}
return undefined;
}
public ensureContextUniqueness(contextName: string) { // ----> (4)
let context: ContextComponent | null = this.parentContext;
while (context !== null) {
if (context.name === contextName) {
throw new Error(`Context ${ this.name } already exist.`);
}
context = context.parentContext;
}
}
}
Inject the parent context component 😲 Check the previous image.
@Optional()
is used to implies that this context may be the first context in the tree, therefore no parents will be found.
@SkipSelf()
is used to tell the dependency resolution to skip the current component injector and start the process from the parent injector because we already have the current context.
Checks if a context with the same name already exists and if so throws an error.
Find a context by a name, starting from the current context, check if its name is equal to the parameter, if not equal repeat the same step with the parent. In the end, if no context is found return undefined. This method will be needed later on with the other components.
Like point 3, but start with the parent context and not the context itself.
Second, modify the ProviderComponent
to grab its context and ensure that it exists.
@Component({
selector: 'provider',
template: '<ng-content></ng-content>'
})
export class ProviderComponent implements OnInit {
@Input() name!: string;
@Input() value?: any;
private providerContext!: ContextComponent;
constructor(
@Optional() private context: ContextComponent | null, // ----> (1)
) { }
ngOnInit(): void {
... code omitted for brevity
if (this.context === null) { // ----> (2)
throw new Error(
'Non of provider ancestors is a context component,
ensure you are using the provider as a context descendant.'
);
}
this.providerContext = this.context.getContext(this.name); // ----> (3)
assertNotNullOrUndefined(this.providerContext, `Provider context ${this.name}`); // ----> (4)
}
public getProvider(contextName: string) { // ----> (5)
let provider: ProviderComponent | null = this;
while (provider !== null) {
if (provider.name === contextName) {
return provider;
}
provider = provider.parentProvider;
}
return undefined;
}
}
ContextComponent
. Angular will search for the nearest context component and inject it, this component will be used to search for another context up in the tree. Third, modify the ConsumerComponent
to grab its context and provider and ensure its context is exists.
@Component({
selector: 'consumer',
template: '<ng-content></ng-content>',
})
export class ConsumerComponent implements OnInit {
@Input() name!: string;
@ContentChild(TemplateRef, { static: true }) templateRef!: TemplateRef<any>;
private consumerContext!: ContextComponent;
private consumerProvider?: ProviderComponent;
constructor(
@Optional() private context: ContextComponent // ----> (1)
) { }
ngOnInit(): void {
... code omitted for brevity
if (this.context === null) { // ----> (2)
throw new Error(
'Non of consumer ancestors is a context component,
ensure you are using the consumer as a context descendant.'
);
}
this.consumerContext = this.context.getContext(this.name); // ----> (3)
this.consumerProvider = this.provider?.getProvider?.(this.name); // ----> (4)
assertNotNullOrUndefined(this.consumerContext, `Consumer context ${this.name}`); // ----> (5)
}
}
ContextComponent
. Angular will search for the nearest context and inject it.RECAP: The code validates the inputs and ensures a context exists and only one exists and is correctly used, also it guides the developer on how to use the context and its components.
Now, it's time for getting the value from the context and the nearest provider to the consumer.
value
If you start this article from the beginning you've read that
The Consumer will re-render whenever a Provider ancestor value changes.
That means the ng-template
should be updated as well and not just building it the first time.
Providing the value might seem easy at first glance since you just need to build the ng-template
and bind a value to it, while that is correct, there're other concerns when it comes to Angular Change Detection, for instance updating the template value in a component that is using OnPush
change detection strategy is difficult than the normal component that uses the Default
change detection strategy, more information about this soon in sepearted section.
For building, there's ViewContainerRef the create and host the ng-template
, also it returns a reference to the ng-template
so we can use it to update its value. more examples and information.
@Component({
selector: 'consumer',
template: '<ng-content></ng-content>',
})
export class ConsumerComponent implements OnInit, OnDestroy {
... code omitted for brevity
private buildTemplate(initialValue: any) { // ----> (1)
this.embeddedView = this.viewContainerRef.createEmbeddedView(this.templateRef, {
$implicit: initialValue
});
}
private updateTemplate(newValue: string) { // ----> (2)
this.embeddedView!.context = {
$implicit: newValue
};
this.embeddedView?.markForCheck();
}
private render(value: any) { // ----> (3)
if (this.embeddedView) {
this.updateTemplate(value);
} else {
this.buildTemplate(value);
}
}
}
ng-template
reference for later use.let-value
, and mark it to be checked in the next change detection cycle.For value changes, normally, the lifecycle that is used to observe @Input
changes is OnChanges
, but since the value is not passed directly to the consumer component it cannot be used there.
The ProviderComponent
will have the ReplaySubject
that will emit the new provider value and the ConsumerComponent
will subscribe to that subject to update its template.
@Component({
selector: 'provider',
template: '<ng-content></ng-content>'
})
export class ProviderComponent implements OnInit, OnDestroy {
private valueState = new ReplaySubject<any>(1); // ----> (1)
ngOnChanges(changes: SimpleChanges): void { // ----> (2)
const valueChange = changes.value;
if (valueChange) {
this.brodcaseValueChanges(valueChange.currentValue);
}
}
... code omitted for brevity
private brodcaseValueChanges(newValue: any) {
this.valueState.next(newValue);
}
public valueChanges() { // ----> (3)
return this.valueState.asObservable();
}
ngOnDestroy(): void {
this.valueState.complete(); // ----> (4)
}
}
ReplaySubject
with a buffer up to 1 so the new consumers will always be able to access the provider's last value.ngOnChanges
lifecycle that was used before to ensure the context name doesn't change to have the logic of detecting the provider value changes.ReplaySubject
to observable for the consumers' components.ProviderComponent
destroy, complete the ReplaySubject
to free up the memory.Now with the ConsumerComponent
part
@Component({
selector: 'consumer',
template: '<ng-content></ng-content>',
})
export class ConsumerComponent implements OnInit, OnDestroy {
private providerValueChangesSubscription?: Subscription; // ----> (1)
ngOnInit(): void {
if (this.consumerProvider) { // ----> (2)
this.providerValueChangesSubscription = this.consumerProvider
.valueChanges()
.subscribe((providerValue) => {
this.render(providerValue); // ----> (3)
});
} else { // ----> (4)
this.render(this.consumerContext.defaultValue);
}
}
... code omitted for brevity
ngOnDestroy(): void {
this.providerValueChangesSubscription?.unsubscribe(); // ----> (5)
}
}
ReplaySubject
on component destroy.Well, you made it so far, good for you! 😄✌️, now you have React Context in Angular, how great was that? Let's see the Angular way of sharing data in the component tree.
Angular does have Dependency Injection framework that provides different approaches to handle a situation where something like React Context API is needed.
In the "The Problem" section, you saw that to pass down a value to the descendants' components you have to declare an @Input
at every component even though that a component might merely act as a wrapper for another component. This actually can be changed by providing an InjectionToken
to the ancestor component and inject that token at any descendant component to utilize the value.
Change the root component to include the InjectionToken
const FamilyNameToken = new InjectionToken('FamilyName');
@Component({
selector: 'app-root',
template: `<app-grandchild> </app-grandchild>`,
providers: [{provide: FamilyNameToken, useValue: 'The Angulars'}]
})
export class AppComponent { }
And for the component that needs the value to inject the InjectionToken
@Component({
selector: 'app-grandchild',
template: `Family Name: {{familyNameValue}}`
})
export class GrandchildComponent {
constructor(@Inject(FamilyNameToken) public familyNameValue: string) { }
}
That might look easy and simple at first, but the catch is when you want to update the value you need to have a kind of RxJS Subject
because Angular will hard inject the value that corresponds to the InjectionToken
into the GrandchildComponent
. The other way is to use a class provider to act as a state holder.
class FamilyName {
private state = new ReplaySubject(1);
public setName(value: string) {
this.state.next(value);
}
public getName() {
return this.state.asObservable();
}
}
The root component will inject the class and sets the value.
@Component({
selector: 'app-root',
template: `<app-grandchild> </app-grandchild>`,
providers: [FamilyName]
})
export class AppComponent {
constructor(public familyName: FamilyName) {
$familyNameState = this.familyName.setName('The Angulars');
}
}
And for the component that needs the value to inject the FamilyName
class and subscribe to the changes.
@Component({
selector: 'app-grandchild',
template: `Family Name: {{$familyNameState|async}}`
})
export class GrandchildComponent {
$familyNameState = this.familyName.getName();
constructor(public familyName: FamilyName) { }
}
Also, you can re-provide the FamilyName
class at any component level so it can act as the ProviderComponent
.
With that said, having a way to pass down a value within the component template it self can reduce the amount of class you will need.
To put the implementation in action, I'll use chat components to illustrate the usage of the context.
Follow the demo to see the result.
Chat Message Component Uses consumer to obtain the message
@Component({
selector: 'app-chat-message',
template: `
<consumer name="ChatContext">
<ng-template let-value>
<h4>{{value.message}}</h4>
</ng-template>
</consumer>
`
})
export class ChatMessageComponent { }
Chat Avatar Component
Uses consumer to obtain the avatar. notice the changeDetection
is changed to OnPush
.
@Component({
selector: 'app-chat-avatar',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<consumer name="ChatContext">
<ng-template let-value>
<img width="50" [src]="value.avatar">
</ng-template>
</consumer>
`
})
export class ColorAvatarComponent { }
Chat Container Component
Group the other components and perhaps for styling and aligning. it uses the provider declared in AppComponent
for the first chat message and a new provider for the second chat message
@Component({
selector: 'app-chat-container',
template: `
<div style="display: flex;">
<app-chat-avatar></app-chat-avatar>
<app-chat-message></app-chat-message>
<provider name="ChatContext" [value]="{name:'Nested Provider Value'}">
<app-chat-message></app-chat-message>
</provider>
</div>
`
})
export class ChatContainerComponent { }
App Component
Declare a context with the name ChatContext with no default value and a provider with initial value chatItem
which will be shared to ChatMessageComponent
and ChatAvatarComponent
.
Clicking on the Change Chat Item button will update the chatItem
reference hence updating the consumers to get the new value.
@Component({
selector: 'app-root',
template: `
<context name="ChatContext">
<provider [value]="chatItem" name="ChatContext">
<app-chat-container></app-chat-container>
</provider>
</context>
<button (click)="updateChatItem()">Change Chat Item</button>
`
})
export class AppComponent {
chatItem = {
message: 'Initial name',
avatar: 'https://icon-library.com/images/avatar-icon-images/avatar-icon-images-4.jpg',
}
updateChatItem() {
const randomInt = Math.round(Math.random() * 10);
this.chatItem = {
message: `Random ${ randomInt }`,
avatar: `https://icon-library.com/images/avatar-icon-images/avatar-icon-images-${ randomInt }.jpg`,
}
}
}
In the Angular Implementation section, there's was an issue when a consumer host component (the component that will be the consumer parent) is using the OnPush
change detection strategy so as fix a ReplaySubject
used to share the value to the consumer component from its nearest provider.
The thing is that OnPush
prevents the component from being auto-checked thus the template of the component won't be updated except on special cases.
@Input
reference changed.Unfortunately, neither of the cases above is applicable on The ConsumerComponent
@Input
for the value because it will be bonded indirectly.Hint: component template implies the template
property in the @Component
decorator and doesn't refer to ng-template
.
The other solution and the initial implementation was to use The DoCheck lifecycle because it is usually used when a component is using OnPush
change detection strategy to detect changes to mutable data structures and mark the component for the next change detection check cycle accordingly.
Moreover, the DoCheck
lifecycle will be invoked during every change detection run but with OnPush
the change detector will ignore the component so it won't be invoked unless it happens manually and again even this is out of the scope because you do not know if the consumer provider value changed or not.
That was just a plus section for the folks who will wonder about that.
If you didn't use state management libraries before, you might find this handy since it somehow solves the same issue, and if you coming from React
background this can be an advantage to have in Angular, nevertheless, Angular can do it on its own with a bit knowledge of dependency injection.
Having such functionality in your app can impart additional value, on the other hand, you have to adapt to the new way of sharing data.