In our last chapter, we talked about how you can create custom logic that is not associated with any particular component but can be used by said components to extend its logic.
This is helpful for sharing logic between components, but isn't the whole story of code reuse within React, Angular, and Vue.
For example, we may want logic associated with a given DOM node without having to create an entire component specifically for that purpose. This exact problem is what a Directive aims to solve.
What Is a Directive
Our "Introduction to Components" chapter mentioned how a component is a collection of structures, styling, and logic that's associated with one or more HTML nodes.
Conversely, a directive is a collection of JavaScript logic that you can apply to a single DOM element.
While this comparison between a directive and a component seems stark, think about it: Components have a collection of JavaScript logic that's applied to a single "virtual" element.
As a result, some frameworks, like Angular, take this comparison literally and use directives under the hood to create components.
Here's what a basic directive looks like in each of the three frameworks:
React
Angular
Vue
React as a framework doesn't quite have the concept of directives built in.
Luckily, this doesn't mean that we, as React developers, need to be left behind. Because a React component is effectively just a JavaScript function, we can use the base concept of a directive to create shared logic for DOM nodes.
We'll continue to cover alternative APIs in React that can do much of the same as directives in other frameworks. In the meantime, it might be beneficial to broaden your horizons and take a glance at what a "true" directive looks like in other frameworks.
You set up a directive in Angular very similarly to how you might construct a component: using the @Directive decorator.
Here, we've told Angular to listen for any sayHi attributes (using a CSS selector) and run a console.log any time an element with said attribute is rendered.
This isn't particularly useful, but demonstrates the most minimal version of what a directive looks like.
Accessing a Directive's Underlying Element
It's frequently more helpful to get a reference to the element that the attribute is present on. To do this, we'll use Angular's dependency injection to ask Angular for an ElementRef that's present within the framework's internals when you create a directive instance.
@Directive({ selector: "[logElement]",})class LogElementDirective { // el.nativeElement is a reference to the HTMLParagraphElement el = inject(ElementRef);}
But oh no! Our directive no longer uses the constructor function, which means that our console.log no longer runs. While we could fix this by adding a constructor after our el:
Directives in Vue must start with v- prefix (which is why our object starts with v) and are dash-cased when presented inside a template. This means that our vSayHi object directive is turned into v-say-hi when used in the template.
Accessing a Directive's Underlying Element
Instead of running a simple console.log on a string, let's use the first argument passed to our directive's created function
to access the underlying HTML element:
Once our apps load up, you should see a console.log execute that prints out the HTMLParagraphElement reference.
You'll notice that these directives' logics are applied to elements through some means of an attribute-like selector, similar to how a component has a named tag associated with it.
Now that we've seen what a directive looks like, let's apply it to some real-world examples.
Basic Directives
Now that we have a reference to the underlying DOM node, we can use that to do various things with the element.
For example, let's say that we wanted to change the color of a button using nothing more than an HTML attribute — we can do that now using the HTMLElement's style property:
When using the created method inside a directive, we can gain access to the underlying DOM node the directive is applied to using the function's arguments.
The first argument that's passed to created is a DOM node reference that we can change the style property of to style our button.
While this is a good demonstration of how you can use an element reference within a directive, styling an element is generally suggested to be done within a CSS file itself, unless you have good reason otherwise.
This is because styling an element through JavaScript can cause issues with server-side rendering, and can also cause layout thrashing if done incorrectly.
See, while a component has a series of side effects associated with it: being rendered, updated, cleaned up, and beyond — so too does an HTML element that's bound to a directive!
Because of this, we can hook into the ability to use side effects within directives so that it focuses when an element is rendered.
React
Angular
Vue
As we already know, we can use built-in React hooks in our custom hooks, which means that we can use useEffect just like we could inside any other component.
Just as you can use the created property on a directive object, you can change this property's name to match any of Vue's component lifecycle method names.
For example, if we wanted to add a cleanup to this directive, we could change mounted to unmounted instead.
Passing Data to Directives
Let's look back at the directive we wrote to add colors to our button. It worked, but that red we were applying to the button element was somewhat harsh, wasn't it?
We could just set the color to a nicer shade of red — say, #FFAEAE — but then what if we wanted to re-use that code elsewhere to set a different button to blue?
To solve this issue regarding per-instance customization of a directive, let's add the ability to pass in data to a directive.
React
Angular
Vue
Because a React Hook is a function at heart, we're able to pass values as we would to any other function:
To pass a value to an Angular directive, we can use the input method, which is the same as a component.
However, one way that a directive's inputs differ from a component's is that you need to prepend the selector value as the Input variable name, like so:
Vue's directives are not simply functions — they are objects that contain functions and can access the value bound to the directive using arguments on each property.
While the first argument of each lifecycle's key is an element reference (el), the second argument will always be the value assigned to the directive.
Now, we can customize the color using incremental updates to the RGB values of a color we're passing.
Passing Multiple Values
While a class instance of Color may be useful in production apps, for smaller projects, it might be nicer to manually pass the r, g, and b values directly to a directive without needing a class.
Just as we can pass multiple values to a component, we can do the same within a directive. Let's see how it's done for each of the three frameworks:
React
Angular
Vue
Once again, the fact that a custom hook is still just a normal function provides us the ability to pass multiple arguments as if they are any other function.
I have to come clean about something: when I said, "A directive's input must be named the same as the attribute's selector," I was lying to keep things simple to explain.
In reality, you can name an input anything you'd like, but then you need to have an empty attribute with the same name as the selector.
The examples we've used to build out basic directives have previously all mutated elements that don't change their visibility; these elements are always rendered on screen and don't change that behavior programmatically.
But what if we wanted a directive that helped us dynamically render an element like we do with our conditional rendering but using only an attribute to trigger the render?
Luckily, we can do that!
Let's build out a basic "feature flags" implementation, where we can decide if we want a part of the UI rendered based on specific values.
The basic idea of a feature flag is that you have multiple different UIs that you'd like to display to different users to test their effectiveness.
For example, say you want to test two different buttons and see which button gets your users to click on more items to purchase:
<button>Add to cart</button>
<button>Purchase this item</button>
You'd start a "feature flag" that separates your audience into two groups, show each group their respective button terminology, and measure their outcome on user's purchasing behaviors. You'd then take these measured results and use them to change the roadmap and functionality of your app.
While the separation of your users into "groups" (or "buckets") is typically done on the backend, let's just use a simple object for this demo.
Let's build a basic version of this in each of our frameworks.
React
Angular
Vue
React has a unique ability that the other frameworks do not. Using JSX, you're able to assign a bit of HTML template into a variable... But that doesn't mean that you have to use that variable.
The idea in a feature flag is that you conditionally render UI components.
See where I'm going with this?
Let's store a bit of UI into a JSX variable and pass it to a custom React Hook that either returns the JSX or null to render nothing, based on the flags named boolean.
Here, we're saying that we want to bind the context key name to a name template variable. This template variable is then accessible to any HTML nodes under
the ng-template.
However, because ng-template doesn't render anything on its own, we'll need to supply a parent to render the ng-template's contents. We do this using the ngTemplateOutlet directive: