JSTL and Build Time vs Render Time
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Jakarta Faces 4.1 (jakarta.faces-api 4.1.2, part of Jakarta EE 11), Eclipse Mojarra 4.1.17
and Apache MyFaces Core 4.1.4. The versions were checked on 2026-10-08. Faces 4.0 behaves the same for everything on this
page; the JSTL tags have been Facelets tag handlers since Faces 2.0.
The behaviour described in the section on a test run was observed for real: a one-page application with a
@ViewScoped bean was served by Apache Tomcat 11.0.26 (embedded) with Weld 6.0.4.Final, once with Mojarra 4.1.17 and once
with MyFaces 4.1.4, and the initial request and several postbacks were driven with curl, while the bean logged the phase in
which each getter was called. The same application was used again on 2026-10-08 to repeat the run on MyFaces and to test the
claims about Ajax and about reordered lists that the first run had not covered; both are reported in the sections where they
appear. Statements that remain unmeasured say so.
The one idea on this page is that a Facelets view is processed twice in two different ways. Once, the XHTML is read
and turned into a tree of components. Later, the tree is walked and turned into HTML. Some tags only exist in the
first step and some only in the second. Mixing them up is the source of most "my c:if does not work" questions.
Two Times: Build and Render
A Jakarta Faces view goes through two distinct stages that are easy to confuse because both are driven by the same XHTML file:
- View build time
-
The Facelets compiler has turned the XHTML file into a tree of tag handlers. Building the view means applying that tree of handlers to a
UIViewRoot: every handler runs once, and the handlers of component tags (h:inputText,h:panelGrid, a composite component) create aUIComponentand add it to the parent. The result is the component tree. A handler sees the EL context of the moment in which it runs, which is the EL context of view build. - View render time
-
The component tree is walked and every component encodes itself. Properties that are value expressions, such as
value,renderedorstyleClass, are evaluated now, as the component asks for them. Aui:repeator ah:dataTablewalks its children once for every row, setting itsvarvariable for each row.
The phases of the request processing lifecycle (see the lifecycle page) say when each of the two takes place. On the first request to a view (a GET), the view is built and rendered inside Render Response: there is nothing to restore, so building is the first step of phase 6. On a postback, the tree is built again in Restore View (phase 1), because the saved state only holds what changed after the build, and decoding, validation and the update of the model run against the tree built there. Render Response then renders it.
The test run shows it. The getter of a bean property used in a c:if was logged with the phase in which it was
called. On Mojarra:
isShow() was called during the test run (Mojarra 4.1.17)GET isShow=false phase=RENDER_RESPONSE 6 the view is built here, and rendered
POST (show checked) isShow=false phase=RESTORE_VIEW 1 the view is built again, before any value is decoded
isShow=false phase=APPLY_REQUEST_VALUES 2
isShow=false phase=PROCESS_VALIDATIONS 3
setShow true phase=UPDATE_MODEL_VALUES 4 the model changes only now
isShow=true phase=RENDER_RESPONSE 6 evaluated again while rendering
The same run on MyFaces 4.1.4 logged these calls, with no call in Restore View:
isShow() was called during the test run (MyFaces 4.1.4)GET isShow=false (3 times) phase=RENDER_RESPONSE(6)
POST (show checked) isShow=false phase=APPLY_REQUEST_VALUES(2)
isShow=false phase=PROCESS_VALIDATIONS(3)
isShow=true phase=UPDATE_MODEL_VALUES(4)
isShow=true (3 times) phase=RENDER_RESPONSE(6)
The log has two lessons. The condition of a build-time tag is evaluated before the submitted values have reached the model: Mojarra called the getter in Restore View, and on both implementations every call before Update Model Values saw the previous state of the model. And the same bean property is read in Render Response too (five times on Mojarra and three on MyFaces for this page), which means that the tags and components of the view are applied or evaluated more than once in a request. That is the implementation’s business; what a page can rely on is only the rule above: the build happens before the submitted values have been applied to the model, and the number and the phase of the calls differ by implementation.
c:if, c:choose, c:forEach, c:set,
ui:include, ui:decorate, ui:param, f:facet"] b3["Component tags create components:
h:inputText, h:panelGrid, ui:repeat,
ui:fragment, composite components"] b1 --> b2 --> b3 end subgraph render["View render time: components encode HTML, expressions evaluated"] direction TB r1["Walk the tree"] r2["Value expressions evaluated:
value, rendered, styleClass, ..."] r3["ui:repeat and h:dataTable set var
once per row"] r1 --> r2 --> r3 end first["GET: Render Response"] --> build post["POST: Restore View"] --> build build --> render update["POST: Update Model Values"] -.->|"runs after the build"| render
Source: Apache MyFaces: Using JSTL tags with Facelets, Jakarta Faces 4.1: 2.2 Standard Request Processing Lifecycle Phases and Build time vs. render time (2008, an early article on the distinction)
What the JSTL Tags Are in Facelets
JSTL (the JSP Standard Tag Library) was written for JSP pages. Facelets does not run JSP, so it ships its own
implementation of a subset of the core library, as tag handlers. MyFaces documents the subset as c:catch,
c:choose, c:if, c:forEach, c:otherwise, c:set and c:when. The namespace is declared like this in Faces 4.x:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets"
xmlns:c="jakarta.tags.core">
Source: Apache MyFaces: Using JSTL tags with Facelets and Jakarta Faces 4.1 specification, 10.4.5 Jakarta Tags Core and Function Tag Libraries
The important consequence is stated plainly in the MyFaces documentation: JSTL tags work as tag handlers and not as JSF components, so they never become part of the component tree. They run during view build and only affect which components the tree gets. They cannot see data that only exists later in the lifecycle.
That has two practical effects:
-
There is nothing to find at render time. No
UIComponentexists for ac:if;findComponent,binding,f:ajax renderand therenderedproperty have nothing to refer to. Ajax cannot re-render ac:if; it can only re-render a component inside one, and only if the component exists after the build (the targets ofrenderare explained on the Ajax page). -
The attributes of a JSTL tag are evaluated while the tree is built, so a variable which a component sets for its children (the
varof aui:repeator ah:dataTable) is not set yet when the tag runs.
JSTL Tags and Their Component Counterparts
Every build-time tag has a component, or a component attribute, that does the similar job at render time. They are not interchangeable, and choosing between them is choosing the time at which the decision is taken.
| JSTL tag (build time) | Render-time counterpart | Difference |
|---|---|---|
|
|
|
|
several components with mutually exclusive |
There is no render-time |
|
|
|
|
|
|
|
no counterpart |
It guards the build of its body. Exceptions at render time are handled by the |
c:out, c:url, c:import and the rest of the library are not provided. Use h:outputText, the ExternalContext
and ui:include instead.
The render-time tags are described one by one on the templating page
(ui:fragment, ui:repeat, ui:param). The specification also says what a build-time condition does to components that
already exist: a component tag under a c:if that turns false on redisplay is removed from the tree, which the
Facelets page measures on both implementations.
A Test Run
The test page below has two sets of constructs, one for each of the times. The bean is view scoped and holds a flag, a list and a counter.
<h:form id="f">
<h:selectBooleanCheckbox id="chk" value="#{demo.show}"/>
<h:commandButton id="apply" value="Apply" action="#{demo.apply}"/>
<p>c:if=[<c:if test="#{demo.show}"><h:outputText value="C-IF"/></c:if>]</p>
<p>rendered=[<h:outputText value="R-ON" rendered="#{demo.show}"/>]</p>
<p>c:forEach=[<c:forEach items="#{demo.items}" var="i"><h:outputText value="#{i};"/></c:forEach>]</p>
<p>ui:repeat=[<ui:repeat value="#{demo.items}" var="i"><h:outputText value="#{i};"/></ui:repeat>]</p>
<p>repeat+c:if=[<ui:repeat value="#{demo.items}" var="i">
<c:if test="#{i eq 'Dune'}"><h:outputText value="#{i}-dune;"/></c:if>
</ui:repeat>]</p>
</h:form>
@Named("demo") @ViewScoped
public class Demo implements Serializable {
private boolean show;
private List<String> items = new ArrayList<>(List.of("Dune", "Emma"));
public boolean isShow() { return show; }
public void setShow(boolean show) { this.show = show; }
public List<String> getItems() { return items; }
public void apply() { }
}
The output of the paragraphs, as served to curl, for the initial request and for the first postback with the
checkbox checked:
GET (show=false)
c:if=[]
rendered=[]
c:forEach=[Dune;Emma;]
ui:repeat=[Dune;Emma;]
repeat+c:if=[] (1)
POST, checkbox checked
c:if=[C-IF]
rendered=[R-ON]
c:forEach=[] (2)
ui:repeat=[Dune;Emma;]
repeat+c:if=[] (1)
POST again, still checked
c:if=[C-IF]
rendered=[R-ON]
c:forEach=[Dune;Emma;] (2)
ui:repeat=[Dune;Emma;]
repeat+c:if=[] (1)
| 1 | Dune is the first item and the condition is true for it, but the c:if ran when i was not defined. It
produced nothing, on every request. This is the classic bug. |
| 2 | The build-time c:if changed its outcome between the Restore View build and the later pass, and on that response
the output of the c:forEach that follows it in the same form was not in its paragraph: the response contained the
text Dune;Emma; at the very start of the form, before the checkbox, and c:forEach=[] in the paragraph. It was in its
place again on the next request. The cause was not investigated; it is consistent with the saved state describing a tree
of a different shape. What the observation shows is that the output of build-time tags is not a function of the current model
alone: on Mojarra it also depends on the earlier requests of the view. |
Everything that ran at render time, rendered and ui:repeat, gave the answer a reader expects every time.
The same page and the same three requests on MyFaces 4.1.4 gave the expected answer every time: after the first postback
c:if=[C-IF], rendered=[R-ON] and c:forEach=[Dune;Emma;], in the paragraph, and repeat+c:if=[] throughout. The
misplaced c:forEach output is therefore a Mojarra 4.1.17 observation, reproduced in one run, not something to expect
elsewhere.
Source: Apache MyFaces: Using JSTL tags with Facelets and the scratch runs described at the top of this page
Classic Bugs and Their Fixes
A Build-Time Tag Reads a Variable That Exists Only at Render Time
This is the bug in the MyFaces documentation, and the one behind callout 1 above. A var set by h:dataTable or
ui:repeat exists only while the component renders a row; a c:if in the body runs once, during the build, when the
variable does not exist, so the condition is false for every row. No error is reported, as an unresolvable
variable is null in EL.
<ui:repeat value="#{catalogue.books}" var="book">
<c:if test="#{book.available}">
<h:outputText value="#{book.title}"/>
</c:if>
</ui:repeat>
The fix is a component that decides at render time:
rendered is evaluated per row<ui:repeat value="#{catalogue.books}" var="book">
<h:outputText value="#{book.title}" rendered="#{book.available}"/>
</ui:repeat>
ui:fragment with a rendered attribute does the same for a group of elements or plain text. The same applies to
c:choose, c:when and c:set inside the body of the iterating component. The test run confirms the bug: the
repeat+c:if=[] paragraph stayed empty on every request on both implementations, although Dune is the first item.
A Build-Time Condition Depends on a Value That Changes in the Request
A condition on a bean property that is changed by the postback itself (a checkbox, a drop-down, an action that
changes a mode) is read when the view is built, before Update Model Values. The tree is built from the old state
of the model. In the test run both implementations showed the new branch in the same response, because the tags are applied
again while rendering (the log above), and Mojarra additionally displaced the output of a neighbouring c:forEach. A page
that depends on that second application depends on the implementation, not on the specification, and a pass that happens to
come out right today is not something to design for.
<h:selectOneMenu value="#{reviewForm.mode}">
<f:selectItems value="#{reviewForm.modes}"/>
<f:ajax render="details"/>
</h:selectOneMenu>
<c:if test="#{reviewForm.mode eq 'detailed'}">
<h:panelGroup id="details">...</h:panelGroup>
</c:if>
Here details exists only if the build created it, and an Ajax request can only update components that are in the tree.
Tested with a view scoped bean and an Ajax request on the menu (render targets :details, details as the client id), both
implementations found details and returned <div id="details">DETAILS-C-IF</div>, again because the tags were applied after
Update Model Values. So the failure the textbook reasoning predicts was not reproduced; the point of the fix below is that it
does not rely on that second application.
rendered decides if it shows<h:selectOneMenu value="#{reviewForm.mode}">
<f:selectItems value="#{reviewForm.modes}"/>
<f:ajax render="details"/>
</h:selectOneMenu>
<h:panelGroup id="details">
<h:panelGroup rendered="#{reviewForm.mode eq 'detailed'}">...</h:panelGroup>
</h:panelGroup>
The outer group is always in the tree, so there is something for the Ajax request to re-render, and the inner group is
evaluated after Update Model Values has run. The same Ajax request returned <div id="details2">DETAILS-RENDERED</div>
on both implementations. The same reasoning applies to c:choose.
c:forEach Over a List That Changes
c:forEach creates components as many times as the list has items when the view is built. The tree is built before
Update Model Values, so a list that is changed by the request (an item added by the action) shows its old size in the
tree built in Restore View. Because each iteration has its own copy of the components, their ids and saved state are
tied to the iteration that created them, which is a hazard for inputs inside a c:forEach over a list which grows, shrinks
or is reordered. The hazard was tested with two inputs, ids built from varStatus, and an action that reversed the list.
<c:forEach items="#{reviewForm.reviews}" var="review">
<h:inputText value="#{review.comment}"/>
</c:forEach>
<ui:repeat value="#{reviewForm.reviews}" var="review">
<h:inputText value="#{review.comment}"/>
</ui:repeat>
ui:repeat and h:dataTable iterate the same single component state over the rows and re-read the model on each
request, which is what a list which changes needs. In the test run, ui:repeat followed an item added by an action
(Book3 appeared in the same response); the c:forEach showed it as well in that run, but a build-time loop gives no
such guarantee, as the flipped c:if above showed.
A related detail: every copy of a component created by a c:forEach must end up with a different id, so an explicit id
has to be built from varStatus: id="comment#{status.index}". Inside ui:repeat this is not needed,
because the component’s client id carries the row index by itself.
The reversal test used this loop and a bean with a list of two reviews with the comments one and two:
<c:forEach items="#{demo.reviews}" var="review" varStatus="st">
<h:inputText id="c#{st.index}" value="#{review.comment}"/>
</c:forEach>
Mojarra 4.1.17 rendered the reversed values in the same two inputs (two, one) and a later Save with new values stored them
in the order shown. MyFaces 4.1.4 failed the request that reversed the list with a 500 response, Component with duplicate id
"f:c1" found, although the ids came from varStatus. A plain Save without reversing worked on both. So no wrong-component
assignment was observed, but the loop was not robust on MyFaces when the list changed, and the cause was not investigated;
the fix remains ui:repeat.
c:set Is Two Different Things
c:set without scope makes var an alias for the value expression. Nothing is evaluated by the tag; the
expression is evaluated every time a component refers to the variable, which is at render time, and again for every
reference. With scope it evaluates the expression once during the build and stores the result in that scope, so every
reference sees the same value.
The test run showed it with a bean whose getter increments a counter and returns it:
<c:set var="alias" value="#{demo.counter}"/>
<c:set var="snap" value="#{demo.counter}" scope="request"/>
<h:outputText value="#{alias}"/>, <h:outputText value="#{alias}"/>
<h:outputText value="#{snap}"/>, <h:outputText value="#{snap}"/>
alias=[2,3] snap=[1,1]
The alias called the getter on each use (2 and 3), the request-scoped variable called it once at build time (1)
and reused the result. So a c:set without scope is no way to cache an expensive getter; and a c:set with scope
keeps its value for the rest of the request even if the model changes later in it, and is stored in the request map,
which every other page of the request can see.
The conventional fix is not to hold a value with c:set at all:
-
To pass a value to a template, a tag file or an included page, use
ui:param. It is scoped to the include and is the same alias semantic. -
To compute an expensive value once, compute it in the bean (a
@PostConstructmethod, a view action, or a field loaded lazily) and keep the getter free of logic.
A View That Changes Shape Between Requests
When the page builds a different tree on different requests, the state saved with the view no longer describes the tree.
Mojarra’s behaviour in the test run (the output of a c:forEach after a flipped c:if was displaced for one response) and the long-standing
reports about it (for example MYFACES-4318, cited by the MyFaces documentation) are two faces of the same fact: a view
that changes shape between requests is working against the way Faces saves state. The remedies are the ones above:
keep the shape stable, and move the changing decision to rendered, or to ui:repeat, or to a component attribute.
Choosing Between a Tag and a Component
Ask one question: can the decision be taken before the form is submitted, and does it stay the same for the life of the view?
| The condition or list depends on | Use | Reason |
|---|---|---|
The row of an iterating component ( |
|
The variable does not exist at build time. |
A bean property changed by a user action or by the postback |
|
Evaluated after Update Model Values, every time the page renders. |
A list that grows, shrinks or is reordered by actions |
|
One set of components, rows read on each request. |
A component that Ajax must be able to re-render |
a component that is always in the tree, with |
Ajax needs an id to find. |
A constant of the deployment or of the request, stable for the life of the view (the project stage, a context parameter, a tag attribute or composite attribute, an application-scoped configuration list) |
|
The answer cannot change between Restore View and Render Response, so both agree. |
Different component types, different numbers of components or different tag attributes that cannot be switched with |
|
Only a build-time tag can change the shape of the tree. |
When a Build-Time Tag Is the Right Choice
A JSTL tag is the right tool when the structure of the page is what is being decided, and the decision is stable, as the MyFaces documentation says: JSTL core tags should only be used when control is needed over the construction of the tree, and even then the pitfalls must be understood. The typical uses are:
-
Generating components from a stable list. A form built from a list of field definitions held in application scope, or the columns of a table built from a configuration, create components of different types and with different attributes: an
h:inputTextfor one definition, anh:selectOneMenuwith itsf:selectItemsfor another. The list does not change while the view is in use.<c:forEach items="#{formDefinition.fields}" var="field" varStatus="status"> <c:choose> <c:when test="#{field.type eq 'choice'}"> <h:selectOneMenu id="field#{status.index}" value="#{formValues.values[field.name]}"> <f:selectItems value="#{field.options}"/> </h:selectOneMenu> </c:when> <c:otherwise> <h:inputText id="field#{status.index}" value="#{formValues.values[field.name]}"/> </c:otherwise> </c:choose> </c:forEach> -
Including or omitting a component that cannot be switched with
rendered, for example af:ajax, anf:eventor anf:validatorthat is registered only when a tag-file attribute asks for it. These are attached to the parent while it is built and have norenderedattribute that would remove them later. -
Conditions on the project stage or on a feature flag fixed at deployment (a development-only panel).
-
Tag files and composite components that take a boolean attribute from the page that uses them and want to include a different set of children for each value.
In each of these, the input to the tag is the same at Restore View and at Render Response, so the two builds agree. If the input can ever differ, the right choice is a component.
The dynamic form above was run with an application-scoped definition of two fields, a text field title and a choice field
genre with three options, and a view-scoped map formValues.values. On Mojarra and MyFaces it rendered an input and a
select with the ids f:field0 and f:field1, and a postback with Dune and Poetry stored {genre=Poetry, title=Dune} in
the map and showed Poetry as the selected option.
A last point on performance: a c:forEach over a large list builds that many components and saves state for each of
them, while ui:repeat and h:dataTable keep a single copy of the children. A build-time loop is therefore for tens
of items that define the structure, not for thousands of rows of data. The cost of the component tree is discussed on the
performance page, and the row model of ui:repeat on the
data tables page.
What Changed Between Versions
| Version | Change relevant to build time and render time |
|---|---|
JSF 1.x with JSP |
The JSTL tags and the Faces tags were both executed by the servlet container while the page rendered, so the interleaving looked like a single pass (see Where the Books Are Outdated). |
JSF 2.0 |
Facelets is the standard view technology; the JSTL core subset ( |
Faces 4.0 |
The JSTL namespace is |
Faces 4.1 |
No change to the semantics. |
MyFaces |
Keeps the switch |
Where the Books Are Outdated
JSTL in Faces pages is a topic of the JSF 2.0 era and of the JSP era before it. Leonard’s JSF 2.0 Cookbook (2010) puts its JSTL material in the same terms as this page, as the difference between build-time and render-time tags, in the chapters about the integration of JSF with other technologies. Older sources written for JSP views describe a different situation: with JSP, the JSTL tags and the JSF tags were both executed by the servlet container while the page was being rendered and the tree was being built at the same time, so the interleaving looked like a single pass. Under Facelets the tree is built first, and JSP views are removed from Faces 4.0, so any advice that depends on that interleaving no longer applies.
MyFaces keeps a switch for the old behaviour. The refactored tag handlers are the default and give better performance
and behaviour; the original ones, with the long-standing bugs of Facelets 1.1.x, can be restored with
org.apache.myfaces.STRICT_JSF_2_FACELETS_COMPATIBILITY, and the MyFaces documentation recommends that only
applications which depend on the old behaviour use it.
References
-
Apache MyFaces: Using JSTL tags with Facelets (pitfalls, limitations, the legacy handler switch)
-
Jakarta Faces 4.1 Specification (2.2 Standard Request Processing Lifecycle Phases)
-
Jakarta Faces 4.1 VDL documentation (
ui:repeat,ui:fragment,ui:param) -
MYFACES-4318 and MYFACES-3810 (referenced from the MyFaces JSTL page)
-
JSF c:forEach vs ui:repeat (Roger Keays, archived copy)
-
Anghel Leonard, JSF 2.0 Cookbook, Packt, 2010