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 a UIComponent and 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, rendered or styleClass, are evaluated now, as the component asks for them. A ui:repeat or a h:dataTable walks its children once for every row, setting its var variable 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:

Phases in which 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:

Phases in which 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.

flowchart TB subgraph build["View build time: tag handlers run, component tree is created"] direction TB b1["XHTML compiled to tag handlers"] b2["Build-time tags run:
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

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">

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 UIComponent exists for a c:if; findComponent, binding, f:ajax render and the rendered property have nothing to refer to. Ajax cannot re-render a c:if; it can only re-render a component inside one, and only if the component exists after the build (the targets of render are 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 var of a ui:repeat or a h: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

c:if

rendered on a component, or ui:fragment rendered

c:if decides whether components exist in the tree; rendered decides whether an existing component encodes. A component with rendered="false" stays in the tree and is skipped on decode, validate and update as well.

c:choose, c:when, c:otherwise

several components with mutually exclusive rendered expressions

There is no render-time choose. Write one rendered per alternative, using the condition and its negation.

c:forEach

ui:repeat, h:dataTable

c:forEach creates one copy of its children in the tree for every item, each with its own component id. ui:repeat has one set of children which is rendered once per item, with the row index inside the client id.

c:set

ui:param (for a variable passed to a template or tag file); a bean property or f:attribute otherwise

c:set is a variable of the page. See below for the two different things it does depending on scope.

c:catch

no counterpart

It guards the build of its body. Exceptions at render time are handled by the ExceptionHandler, described on the exception handling page.

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.

index.xhtml: the same decision taken at build time and at render time
<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>
Demo.java: the bean behind the page
@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:

Output, Mojarra 4.1.17
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.

The bug: the condition is evaluated before any row exists
<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:

The fix: 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.

The bug: the branch is chosen from a value that the same request is about to change
<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.

The fix: the component always exists and 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.

The bug: one input component per item, created once at build time
<c:forEach items="#{reviewForm.reviews}" var="review">
  <h:inputText value="#{review.comment}"/>
</c:forEach>
The fix: one component, rendered per row
<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:

The loop that was tested
<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:

Test: the counter is read through an alias and through a request-scope variable
<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}"/>
Output of the first request, Mojarra 4.1.17 and MyFaces 4.1.4 (identical)
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 @PostConstruct method, 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 (var)

rendered, ui:fragment

The variable does not exist at build time.

A bean property changed by a user action or by the postback

rendered, ui:fragment

Evaluated after Update Model Values, every time the page renders.

A list that grows, shrinks or is reordered by actions

ui:repeat, h:dataTable

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 rendered inside

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)

c:if, c:choose, c:forEach are acceptable

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 rendered

c:if, c:choose, c:forEach

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:inputText for one definition, an h:selectOneMenu with its f:selectItems for 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 a f:ajax, an f:event or an f:validator that is registered only when a tag-file attribute asks for it. These are attached to the parent while it is built and have no rendered attribute 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 (c:if, c:choose, c:forEach, c:set, c:catch) is implemented as Facelets tag handlers, which run at view build time. ui:repeat and ui:fragment are the render-time tools.

Faces 4.0

The JSTL namespace is jakarta.tags.core; the old xmlns.jcp.org and java.sun.com URIs are still accepted. JSP views are removed.

Faces 4.1

No change to the semantics. ui:repeat documents rowStatePreserved in the VDL.

MyFaces

Keeps the switch org.apache.myfaces.STRICT_JSF_2_FACELETS_COMPATIBILITY for the pre-refactoring tag handlers (Where the Books Are Outdated).

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