Post contents
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.
Remember from our "Element Reference" chapter that you can use a function associated with an element's ref
property. We'll use this concept alongside the idea of a custom hook to create an API to add logic to an HTML element:
const useLogElement = () => { const ref = (el) => console.log(el); return { ref };};const App = () => { const { ref } = useLogElement(); return <p ref={ref}>Hello, world</p>;};
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.
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:
- React
- Angular
- Vue
const useStyleBackground = () => { const ref = (el) => { el.style.background = "red"; }; return { ref };};const App = () => { const { ref } = useStyleBackground(); return <button ref={ref}>Hello, world</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.
Side Effect Handlers in Directives
Previously, in the book, we've explored adding a focus
event when an element is rendered. However, in this chapter, we explicitly had to call a focus
method. What if we could have our button
focus itself immediately when it's rendered onto the page?
Luckily, with directives, we can!
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.
const useFocusElement = () => { const [el, setEl] = useState(); useEffect(() => { if (!el) return; el.focus(); }, [el]); const ref = (localEl) => { setEl(localEl); }; return { ref };};const App = () => { const { ref } = useFocusElement(); return <button ref={ref}>Hello, world</button>;};
Truthfully, this is a bad example for
useEffect
. Instead, I would simply runlocalEl.focus()
inside of theref
function.
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:
const useStyleBackground = (color) => { const ref = (el) => { el.style.background = color; }; return { ref };};const App = () => { const { ref } = useStyleBackground("#FFAEAE"); return <button ref={ref}>Hello, world</button>;};
Passing JavaScript Values
Similar to how you can pass any valid JavaScript object to a component's inputs, you can do the same with a directive.
To demonstrate this, let's create a Color
class that includes the following properties:
class Color { constructor(r, g, b) { this.r = r; this.g = g; this.b = b; }}
Then, we can render out this color inside our background styling directive:
- React
- Angular
- Vue
class Color { constructor(r, g, b) { this.r = r; this.g = g; this.b = b; }}const colorInstance = new Color(255, 174, 174);const useStyleBackground = (color) => { const ref = (el) => { el.style.background = `rgb(${color.r}, ${color.g}, ${color.b})`; }; return { ref };};const App = () => { const { ref } = useStyleBackground(colorInstance); return <button ref={ref}>Hello, world</button>;};
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.
const useStyleBackground = (r, g, b) => { const ref = (el) => { el.style.background = `rgb(${r}, ${g}, ${b})`; }; return { ref };};const App = () => { const { ref } = useStyleBackground(255, 174, 174); return <button ref={ref}>Hello, world</button>;};
Conditionally Rendered UI via Directives
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.
const flags = { addToCartButton: true, purchaseThisItemButton: false,};
In this instance, we might render something like:
<button id="addToCart">Add to Cart</button>
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.
const flags = { addToCartButton: true, purchaseThisItemButton: false,};const useFeatureFlag = ({ flag, enabledComponent, disabledComponent = null,}) => { if (flags[flag]) { return { comp: enabledComponent }; } return { comp: disabledComponent, };};function App() { const { comp: addToCartComp } = useFeatureFlag({ flag: "addToCartButton", enabledComponent: <button>Add to cart</button>, }); const { comp: purchaseComp } = useFeatureFlag({ flag: "purchaseThisItemButton", enabledComponent: <button>Purchase this item</button>, }); return ( <div> {addToCartComp} {purchaseComp} </div> );}
Challenge
This code was functional and led to a nice user experience, but the tooltip wasn't broken out into its own components, making it challenging to share the code elsewhere.
Let's refactor that code so that we can add a tooltip
directive so that adding a tooltip is as easy as adding an attribute! To make this challenge more focused on what we've learned in this chapter, let's simplify the design of our tooltip to something like the following:
To build this, we'll need to:
- Add a tooltip directive
- Allow for an input to the directive that's the contents
- Bind the tooltip to a button element
We can use the following CSS for the tooltip itself:
.tooltip { position: absolute; background-color: #333; color: #fff; padding: 8px; border-radius: 4px; z-index: 1000;}
Let's get started.
- React
- Angular
- Vue
To avoid having to add manual event listeners to our button, let's pass in a button and the contents of the tooltip using properties on a useTooltip
custom hook:
const useTooltip = ({ tooltipContents, innerContents }) => { const [isVisible, setIsVisible] = useState(false); const targetRef = useRef(); const tooltipRef = useRef(); const showTooltip = () => { setIsVisible(true); }; const hideTooltip = () => { setIsVisible(false); }; useEffect(() => { if (!isVisible || !tooltipRef.current || !targetRef.current) return; const targetRect = targetRef.current.getBoundingClientRect(); tooltipRef.current.style.left = `${targetRect.left}px`; tooltipRef.current.style.top = `${targetRect.bottom}px`; }, [isVisible]); return ( <div> <div ref={targetRef} onMouseEnter={showTooltip} onMouseLeave={hideTooltip} > {innerContents} </div> {isVisible && createPortal( <div ref={tooltipRef} className="tooltip"> {tooltipContents} </div>, document.body, )} </div> );};const App = () => { const tooltip = useTooltip({ innerContents: <button>Hover me</button>, tooltipContents: "This is a tooltip", }); return ( <div> {tooltip} <style children={` .tooltip { position: absolute; background-color: #333; color: #fff; padding: 8px; border-radius: 4px; z-index: 1000; } `} /> </div> );};