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.
Table Of Content
- What Is React Context API
- The Problem
- The Solution
- How Does React Context API Work
- Angular implementation
- The Angular Way
- Bonus Part: The Issue With OnPush
What Is React Context API
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.
AppComponent passes the value to the Parent component
Parent component passes the value to the Child component
Child component passes the value to the Grandchild component
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
And for the component that needs the value
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.
How Does React Context API Work
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.
The provider have
value props that will passed down to the consumers.
The consumer takes a function with the Provider value as an argument, the function will be called (re-render 🙃) whenever the Provider value changes.
You might want to know that this is not the only way to consume context, there’s
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
and ended up using them like this
I’ll explain each component in detail soon.
Utility function for strict mode people 😅
The Context Component
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
- Have multiple contexts that can be used without interfering with each other.
- The provider and consumer to find their Context easily by looking the name up.
- Ensures a provider and a consumer are defined under their context and not in any other place.
- Prevents duplicated contexts.
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 ).
- ng-content to project the content as is.
- Name of the context. reasons above 😁
valuethat will be provided to the consuming components in case there’s no provider for this context.
- Ensures the context name is a string and not empty. The same check will be used in the other components.
- The name cannot be changed since the code should adhere to the React approach, nevertheless, this is totally up to you. the same check will be used in the other components.
The Provider Component
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.
- Name of the context. The name is needed in order to know to which context it belongs.
valuethat will be provided to the consuming components.
- The provider is valuable as long it holds a value, if at first it doesn’t so there’s no point in having it, let the consumers rely on a different provider or the default value provided when establishing the context
The Consumer Component
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.
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.
- Name of the context. The name is needed in order to know to which context it belongs.
- The template reference,
static: trueused to be able to get it in
ng-templateis 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.
- Context should be unique, you cannot have multiple contexts with the same name.
- Providers and Consumers must have a context.
First, adding a method to
ContextComponent that will ensure no other context with the same name is exist.
- 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.
- Inject the
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.
- Check if there’s context at all before searching for the provider context. this might be helpful so you immediately know you missed adding the context.
- Get the provider context and assign it to its instance.
- Ensure the provider has context.
- Find a provider by a context name, starting from the current provider, check if its name is equal to the parameter, if not equal repeat the same step with the parent. In the end, if no provider is found it is okay to return undefined to state that a context doesn’t have a provider since it’s optional. This method will be needed soon in the consumer component.
Third, modify the
ConsumerComponent to grab its context and provider and ensure its context is exists.
- Inject the
ContextComponent. Angular will search for the nearest context and inject it.
- Check if there’s context at all before searching for the consumer context. this might be helpful so you immediately know you missed adding the context.
- Get the consumer context and assign it to its instance.
- Ensure the consumer has a context.
- Get the consumer nearest provider and assign it to the consumer instance. This will be used next to observe provider value changes.
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.
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.
- Create the template passing it the initial value (which could be its context default value or its nearest provider current value) and stores the
ng-templatereference for later use.
- Update the template value, the
let-value, and mark it to be checked in the next change detection cycle.
- Wrapper method to either update the template in case it is already there or build it otherwise.
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.
ProviderComponent will have the
ReplaySubject that will emit the new provider value and the
ConsumerComponent will subscribe to that subject to update its template.
- Initialize the
ReplaySubjectwith a buffer up to 1 so the new consumers will always be able to access the provider’s last value.
- Modify the
ngOnChangeslifecycle that was used before to ensure the context name doesn’t change to have the logic of detecting the provider value changes.
- Convert the
ReplaySubjectto observable for the consumers’ components.
ProviderComponentdestroy, complete the
ReplaySubjectto free up the memory.
Now with the
- A field to hold the provider subscription to unsubscribe on the component destroy.
- Check if a provider is defined to subscribe to its value changes.
- If there’s a provider re-render on its value changes
- If there’s no provider render only once with the context default value.
- Unsubscribe from the provider
ReplaySubjecton 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.
The Angular Way
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
And for the component that needs the value to inject the InjectionToken
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.
The root component will inject the class and sets the value.
And for the component that needs the value to inject the
FamilyName class and subscribe to the changes.
Also, you can re-provide the
FamilyName class at any component level so it can act as the
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
Chat Avatar Component
Uses consumer to obtain the avatar. notice the
changeDetection is changed to
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
Declare a context with the name ChatContext with no default value and a provider with initial value
chatItem which will be shared to
Clicking on the Change Chat Item button will update the
chatItem reference hence updating the consumers to get the new value.
Bonus Part: The Issue With OnPush
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.
- One of the components
- An event handler of the component was triggered.
- An observable linked to the component template via the async pipe emits.
Unfortunately, neither of the cases above is applicable on The
- It doesn’t have an
@Inputfor the value because it will be bonded indirectly.
- It doesn’t have any event handler.
- And no observable can be linked to its template since we project the content as is.
Hint: component template implies the
template property in the
@Component decorator and doesn’t refer to
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.
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.
The drawing was made via Excalidraw.