Using JSX
Stencil components are rendered using JSX, a popular, declarative template syntax. Each component has a render function that returns a tree of components that are rendered to the DOM at runtime.
By default, Stencil sets up projects for JSX automatically. If you're setting up a project manually, add the following to your tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@stencil/core"
}
}
Basics
The render function
The render function is used within your component to output a tree of elements that will be drawn to the screen.
class MyComponent {
render() {
return (
<div>
<h1>Hello World</h1>
<p>This is JSX!</p>
</div>
);
}
}
In this example we're returning the JSX representation of a div, with two child elements: an h1 and a p.
Host Element
If you want to modify the host element itself, such as adding a class or an attribute to the component itself, use the <Host> functional component. Check for more details here
Data Binding
Components often need to render dynamic data. To do this in JSX, use { } around a variable:
render() {
return (
<div>Hello {this.name}</div>
)
}
If you're familiar with ES6 template variables, JSX variables are very similar, just without the $:
//ES6
`Hello ${this.name}`
//JSX
Hello {this.name}
Conditionals
If we want to conditionally render different content, we can use JavaScript if/else statements:
Here, if name is not defined, we can just render a different element.
render() {
if (this.name) {
return ( <div>Hello {this.name}</div> )
} else {
return ( <div>Hello, World</div> )
}
}
Additionally, inline conditionals can be created using the JavaScript ternary operator:
render() {
return (
<div>
{this.name
? <p>Hello {this.name}</p>
: <p>Hello World</p>
}
</div>
);
}
Please note: Stencil reuses DOM elements for better performance. Consider the following code:
{someCondition
? <my-counter initialValue={2} />
: <my-counter initialValue={5} />
}
The above code behaves exactly the same as the following code:
<my-counter initialValue={someCondition ? 2 : 5} />
Thus, if someCondition changes, the internal state of <my-counter> won't be reset and its lifecycle methods such as componentWillLoad() won't fire. Instead, the conditional merely triggers an update to the very same component.
If you want to destroy and recreate a component in a conditional, you can assign the key attribute. This tells Stencil that the components are actually different siblings:
{someCondition
? <my-counter key="a" initialValue={2} />
: <my-counter key="b" initialValue={5} />
}
This way, if someCondition changes, you get a new <my-counter> component with fresh internal state that also runs the lifecycle methods componentWillLoad() and componentDidLoad().
Click both counters a few times, then Toggle. The counter without a key keeps its count, and the one with a key starts again from its initialValue:
import { Component, Prop, State } from '@stencil/core';
@Component({ tag: 'my-counter' })
export class MyCounter {
@Prop() initialValue = 0;
@State() count = 0;
componentWillLoad() {
this.count = this.initialValue;
}
render() {
return <button onClick={() => this.count++}>Count: {this.count}</button>;
}
}
import { Component, State } from '@stencil/core';
@Component({ tag: 'my-toggle-demo' })
export class MyToggleDemo {
@State() someCondition = true;
render() {
return (
<div>
<button onClick={() => (this.someCondition = !this.someCondition)}>Toggle</button>
<p>
Without key:{' '}
{this.someCondition
? <my-counter initialValue={2} />
: <my-counter initialValue={5} />
}
</p>
<p>
With key:{' '}
{this.someCondition
? <my-counter key="a" initialValue={2} />
: <my-counter key="b" initialValue={5} />
}
</p>
</div>
);
}
}
<script type="module" src="./my-counter.js"></script>
<script type="module" src="./my-toggle-demo.js"></script>
<my-toggle-demo></my-toggle-demo>
Slots
A <slot> in your JSX marks where content passed into the component goes. See Slots for named slots, fallback content, and how slots work with and without shadow DOM.
Dealing with Children
The children of a node in JSX correspond at runtime to an array of nodes,
whether they are created by mapping across an array with
Array.prototype.map
or simply declared as siblings directly in JSX. This means that at runtime the
children of the two top-level divs below (.todo-one and .todo-two) will be
represented the same way:
render() {
return (
<>
<div class="todo-one">
{this.todos.map((todo) => (
<span>{ todo.taskName }</span>
))}
</div>
<div class="todo-two">
<span>{ this.todos[0].taskName }</span>
<span>{ this.todos[1].taskName }</span>
</div>
</>
)
}
If this array of children is dynamic, i.e., if any nodes may be added,
removed, or reordered, then it's a good idea to set a unique key attribute on
each element like so:
render() {
return (
<div>
{this.todos.map((todo) => (
<div key={todo.uid}>
<div>{todo.taskName}</div>
</div>
))}
</div>
)
}
When nodes in a children array are rearranged Stencil makes an effort to
preserve DOM nodes across renders but it isn't able to do so in all cases.
Setting a key attribute lets Stencil ensure it can match up new and old
children across renders and thereby avoid recreating DOM nodes unnecessarily.
Do not use an array index or some other non-unique value as a key. Try to ensure that each child has a key which does not change and which is unique among all its siblings.
Automatic Key Insertion
During compilation Stencil will automatically add key attributes to any JSX nodes in your component's render method which are not nested within curly braces. This allows Stencil’s runtime to accurately reconcile children when their order changes or when a child is conditionally rendered.
For instance, consider a render method looking something like this:
render() {
return (
<div>
{ this.disabled && <div id="no-key">no key!</div> }
<div id="slot-wrapper">
<slot/>
</div>
</div>
);
}
While it might seem like adding a key attribute to the #slot-wrapper div
could help ensure that elements will be matched up correctly when the component
re-renders, this is actually superfluous because Stencil will automatically add
a key to that element when it compiles your component.
The Stencil compiler can only safely perform automatic key insertion in certain scenarios where there is no danger of the keys accidentally causing elements to be considered different when they should be treated the same (or vice versa).
In particular, the compiler will not automatically insert key attributes if a
component's render method has more than one return statement or if it
returns a conditional
expression.
Additionally, the compiler will not add key attributes to any JSX which is
found within curly braces ({ }).
Handling User Input
Stencil uses native DOM events.
Here's an example of handling a button click. Note the use of the Arrow function.
...
export class MyComponent {
private handleClick = () => {
alert('Received the button click!');
}
render() {
return (
<button onClick={this.handleClick}>Click Me!</button>
);
}
}
Here's another example of listening to input change. Note the use of the Arrow function.
...
export class MyComponent {
private inputChanged = (event: Event) => {
console.log('input changed: ', (event.target as HTMLInputElement).value);
}
render() {
return (
<input onChange={this.inputChanged}/>
);
}
}
Complex Template Content
So far we've seen examples of how to return only a single root element. We can also nest elements inside our root element
In the case where a component has multiple "top level" elements, the render function can return an array.
Note the comma in between the <div> elements.
render() {
return ([
// first top level element
<div class="container">
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>,
// second top level element, note the , above
<div class="another-container">
... more html content ...
</div>
]);
}
Alternatively, wrap them in a fragment, <>...</>, which groups elements without adding a wrapper node to the DOM. No commas are needed:
render() {
return (
<>
<div class="container">
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
<div class="another-container">
... more html content ...
</div>
</>
);
}
It is also possible to use innerHTML to inline content straight into an element. This can be helpful when, for example, loading an svg dynamically and then wanting to render that inside of a div. This works just like it does in normal HTML:
<div innerHTML={svgContent}></div>
Getting a reference to a DOM element
In cases where you need to get a direct reference to an element, like you would normally do with document.querySelector, you might want to use a ref in JSX. Lets look at an example of using a ref in a form:
@Component({
tag: 'app-home',
})
export class AppHome {
textInput!: HTMLInputElement;
handleSubmit = (event: Event) => {
event.preventDefault();
console.log(this.textInput.value);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={(el) => this.textInput = el as HTMLInputElement} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
In this example we are using ref to get a reference to our input ref={(el) => this.textInput = el as HTMLInputElement}. We can then use that ref to do things such as grab the value from the text input directly this.textInput.value.
Explicitly setting attributes or properties
By default, Stencil tries to intelligently determine whether to set an attribute or (more commonly) a property on an element when using JSX.
However, in some cases you may want to explicitly set one or the other. You can do this by using the attr: or prop: prefixes. For example:
<input attr:value="Hello" />
will set the value attribute on the input element, while:
<input prop:value="Hello" />
will explicitly set the value property on the input element.
For an input's value, the difference shows once someone has typed. Type in both inputs, then click Toggle. The prop:value input shows the new value. The attr:value input keeps what you typed, because the attribute is only the input's default value:
import { Component, State } from '@stencil/core';
@Component({ tag: 'my-cmp' })
export class MyCmp {
@State() greeting = 'Hello';
private toggle = () => {
this.greeting = this.greeting === 'Hello' ? 'Goodbye' : 'Hello';
};
render() {
return (
<div>
<input attr:value={this.greeting} />
<input prop:value={this.greeting} />
<button onClick={this.toggle}>Toggle</button>
</div>
);
}
}
Avoid Shared JSX Nodes
The renderer caches element lookups in order to improve performance. However, a side effect from this is that the exact same JSX node should not be shared within the same renderer.
In the example below, the sharedNode variable is reused multiple times within the render() function. The renderer is able to optimize its DOM element lookups by caching the reference, however, this causes issues when nodes are reused. Instead, it's recommended to always generate unique nodes like the changed example below.
@Component({
tag: 'my-cmp',
})
export class MyCmp {
render() {
- const sharedNode = <div>Text</div>;
return (
<div>
- {sharedNode}
- {sharedNode}
+ <div>Text</div>
+ <div>Text</div>
</div>
);
}
}
Alternatively, creating a factory function to return a common JSX node could be used instead since the returned value would be a unique instance. For example:
@Component({
tag: 'my-cmp',
})
export class MyCmp {
getText() {
return <div>Text</div>;
}
render() {
return (
<div>
{this.getText()}
{this.getText()}
</div>
);
}
}