definitionExpression, filter, featureEffect — and the feature count you can actually quote
Every serious map application eventually grows a filter. A zoning code, a date range, a district, a status. In the ArcGIS Maps SDK for JavaScript there are three properties that will all appear to narrow the layer, and the samples that demonstrate them look almost interchangeable. They are not interchangeable. They act at different points in the pipeline, so the choice between them decides what the map costs to draw and whether the number in your results panel is a fact or a guess. One of the three is also routinely mistaken for an access control, which is a third reason to be precise about which is which. This is the working reference for choosing.
Three levers, one decision
Start from what each property changes. Not what it looks like on screen — all three can produce a map showing only C-2 parcels — but what it changes about the data underneath.
| Property | Lives on | Changes | Reach for it when |
|---|---|---|---|
definitionExpression |
The layer | What this browser ever receives | The restriction is permanent for this app, or the dataset is too big to bring down |
filter |
The layer view | What draws, out of what the layer view already holds | The user is toggling criteria and you want the map to respond without a round trip |
featureEffect |
The layer or the layer view | How it draws. Nothing is removed | Removing the non-matches would destroy the context that makes the matches readable |
// 1. Changes what this browser ever receives. layer.definitionExpression = "zoning = 'C-2'"; // 2. Changes what draws, out of what the layer view already holds. layerView.filter = new FeatureFilter({ where: "zoning = 'C-2'" }); // 3. Changes how it draws. The non-matches are still there. layerView.featureEffect = new FeatureEffect({ filter: new FeatureFilter({ where: "zoning = 'C-2'" }), excludedEffect: "grayscale(100%) opacity(30%)" });
Both support classes date from 4.22, though layer view filtering itself is older —
FeatureLayerView.filter has been there since 4.11.
import FeatureFilter from "@arcgis/core/layers/support/FeatureFilter.js"; import FeatureEffect from "@arcgis/core/layers/support/FeatureEffect.js";
definitionExpression changes what reaches the browser
The querying and filtering guide draws the line that matters, and it is a line about the
layer, not about the property. Layers split into two families. Server-side layers
— FeatureLayer, OGCFeatureLayer,
SceneLayer, StreamLayer — fetch only
what they need on load and go back to the service for more as required.
Client-side layers — CSVLayer,
GeoJSONLayer, WFSLayer, and a
FeatureLayer built from an array of graphics through its
source property — pull everything at once and never talk to a
server again.
On a server-side layer, the guide is explicit: setting a
definitionExpression “triggers a network request to fetch
features that satisfy the definition expression.” That is the whole reason to use it.
Features outside the expression are never downloaded, never rendered, never counted, and never
left sitting in the browser. On a client-side layer the same property evaluates locally, against
everything the layer already holds — same syntax, completely different payload story.
One more thing makes definitionExpression the natural floor of the
stack: it is honored by everything above it. If a layer carries a definition expression, the
guide notes that all layer view queries and filters honor it too. You can set the permanent
restriction once, on the layer, and reason about every filter and query above it as operating
inside that boundary.
definitionExpression is a writable client property — the
reference notes it can be set before the layer loads or after it has been added to the
map. It becomes a where clause on the request the browser sends. The
service endpoint is unchanged, it is sitting in the network tab, and one line in the console
removes the clause. It keeps data off the wire for a cooperative client, which is a real and
useful thing; it does not keep data away from anyone who does not wish to cooperate. If the
requirement is “the public site must not expose the parcels we have flagged,” the
answer has to be enforced on the server. That means a hosted feature layer view whose view
definition is stored with the item, an ArcGIS Enterprise database view or filtered service, or a
proxy that injects the clause and never exposes the raw service URL. Esri’s guidance for
hosted views is that the definitions “are saved with the hosted feature layer view”
— which is exactly the property a client-set expression lacks.One practical note, because a clause in an article is cheap and the mechanism is not. Creating a hosted feature layer view requires you to own the source hosted feature layer or to be an organization administrator, and the source has to be a hosted layer rather than a referenced ArcGIS Server service. A developer consuming a partner’s service cannot do it, and the right next step there is a conversation with the data owner rather than a client-side workaround. On cost: hosted views reference the existing data rather than copying it, so they add no credits, and feature layer queries are not a credit-metered capability. The round trips in this article cost latency, not credits.
filter changes what draws, from what already arrived
A FeatureFilter assigned to
FeatureLayerView.filter is the fast lever. There is no network
request; the layer view re-evaluates the features it is already holding and redraws. For a
criteria panel the user is dragging a slider on, that responsiveness is the entire point.
The class carries seven properties and no more: where,
geometry, spatialRelationship,
distance, units,
timeExtent and objectIds. That is a
complete list, and the absences tell you what a filter is for. There is no
outFields, no orderByFields, no
statistics. A filter affects visibility. It returns nothing.
The reference is blunt about the cost of the speed. A FeatureFilter
“runs against features that are available for drawing on the client-side.” Those
features are optimized for performance, so filter results are, in the docs’ own words,
“not always accurate.” The same paragraph names the remedy: use the layer’s own
queryFeatures() method, or a
definitionExpression, when the filter has to run against every
feature.
Two consequences follow, and both are invisible in a demo built on a two-hundred-feature
layer. Attribute values are compared in the browser, and the guide warns that client-side
attribute values are case sensitive — a where clause a service
happily matched can quietly match nothing once it is being evaluated on the client. And
geometries held by a layer view are generalized for drawing, so a spatial filter or a client-side
area calculation returns results that the guide describes as imprecise, and that change as the
user zooms.
outFields. The fetched set is
readable at FeatureLayerView.availableFields, and the query reference
states the consequence plainly: ensure the fields being queried are in that list, “otherwise,
the query may fail or return incomplete results.” Name the fields at layer construction:
new FeatureLayer({ portalItem: { id }, outFields: ["zoning", "yearBuilt"] }).
You can also set outFields after the layer has loaded, and
Esri’s own samples do. But availableFields is populated only
once the layer view finishes updating, so the constructor version is the one that is already
correct when your first query runs.featureEffect, when hiding is the wrong answer
Hiding non-matching features is a reflex, and on a lot of maps it is the wrong call. Show a
planner the eleven parcels that match their criteria with everything else removed and you have
taken away the thing that made the eleven meaningful: where they sit relative to everything they
do not match. FeatureEffect exists for that case. It takes the same
FeatureFilter and, instead of removing the non-matches, applies an
effect to each side of the split — includedEffect for features
that pass, excludedEffect for features that fail.
// Matches at full strength; everything else still on the map, grayed back. layerView.featureEffect = new FeatureEffect({ filter: new FeatureFilter({ where: "zoning = 'C-2'" }), excludedEffect: "grayscale(100%) opacity(30%)" }); // Or emphasize the matches instead of suppressing the rest. layerView.featureEffect = new FeatureEffect({ filter: new FeatureFilter({ where: "BoroughEdge='true'" }), includedEffect: "drop-shadow(3px, 3px, 3px, black)", excludedEffect: "blur(1px) brightness(65%)" });
Both effect properties are typed Effect, which accepts a CSS
filter string as shown or an array of scale stops, so the treatment can change as the user zooms
rather than being fixed for the whole map.
Set the effect on the layer and the layer view inherits it. Set it on both and the layer view wins. That inheritance is more useful than it looks: put the effect on the layer when it is a property of the map you are authoring, and on the layer view when it belongs to what this particular user is doing right now.
FeatureEffect set on a layer view cannot be persisted at all.
Even on the layer, excludedLabelsVisible “must be set to
true in order to persist the FeatureEffect to a WebMap,” and its
default is false. And the effect “can only be persisted to a
WebMap if an attribute filter is the only property set on FeatureFilter” — so the
geometry-driven effect in Esri’s own samples cannot be saved into a web map under any
arrangement. Author it in code, or accept that it lives for the session.Keep featureEffect straight from its neighbor
effect, which applies the same kind of treatment to the whole
layer with no filter and no included-versus-excluded split. When several are set at once the
reference gives the order they compose in — featureEffect, then
effect, then opacity, then
blendMode. If a layer-wide effect is
already washing everything out, an excludedEffect stacked underneath
it will not read as strongly as it does in the reference examples.
The three flags that decide whether to trust the client
Here is where the choice stops being cosmetic. Once a filter is on screen, somebody wants a number next to it — matches found, parcels affected, sites in range. The obvious move is to ask the layer view, because it is right there and it answers instantly. The obvious move is often wrong, and version 4.29 added three read-only booleans whose entire job is to tell you when.
| Flag | True means | False means |
|---|---|---|
hasAllFeatures |
The layer view holds every feature from the source, so layer view queries run against the entire dataset | It holds only what was needed for drawing; queries run against that subset |
hasAllFeaturesInView |
Everything relevant to the current extent arrived, so a query scoped to the extent is answerable on the client | Some queries failed, or the layer holds a large number of features and is configured with a display filter |
hasFullGeometries |
Geometries are present at full resolution, without quantization | Geometries are generalized for drawing — do not measure them |
The guidance attached to each is the same shape: if the flag is true, the layer view can
answer; if it is false, ask the layer, which goes to the service. Note the third one in
particular. hasFullGeometries is the flag that separates “how
many parcels intersect this buffer” from “how many acres do they cover.” The
first can tolerate a generalized outline. The second cannot, and there is nothing in the returned
geometry to warn you that it has been simplified.
All three settle only after the layer view has finished fetching, which is why every
construction below waits first. And be clear about what a branch on them buys you. When
hasAllFeatures is true the reference says a layer view query runs
against the entire dataset, so the answer is right either way — the branch is saving a round
trip, not rescuing correctness. Whether a given layer loads entirely on the client is an internal
decision the SDK makes rather than a contract, so a branch taking the fast path today can take
the slow path after an upgrade with no change to the answer, only to the latency.
A real job — a parcel panel that reports a defensible count
Take a county parcel layer of a couple of hundred thousand features and a side panel where an analyst sets criteria: commercial zoning, built before 1980. The panel shows the matches on the map and a count at the top.
The naive build sets a layer view filter and reads
layerView.queryFeatureCount(). It is wrong in a way that is almost
designed to escape review. It returns a number. The number looks plausible. And it counts only
the features the layer view happens to be holding, which is a function of where the user has
already panned and zoomed. The same criteria therefore produce different numbers in two sessions,
and neither is the answer to the question the analyst asked.
The correct build separates the two questions. What the analyst sees is a drawing concern and belongs on the client, where it is instant. What the analyst quotes is a data question and belongs wherever it can actually be answered.
import FeatureLayer from "@arcgis/core/layers/FeatureLayer.js"; import FeatureFilter from "@arcgis/core/layers/support/FeatureFilter.js"; import FeatureEffect from "@arcgis/core/layers/support/FeatureEffect.js"; import * as reactiveUtils from "@arcgis/core/core/reactiveUtils.js"; // The fields the client-side effect reads must be fetched. Name them at // construction so they are in availableFields before the first query runs. const layer = new FeatureLayer({ portalItem: { id: PARCELS_ITEM_ID }, outFields: ["zoning", "yearBuilt"] }); let seq = 0; // monotonic request token - see the note on stale responses below async function applyCriteria(view, layer, panel, where) { const ticket = ++seq; const layerView = await view.whenLayerView(layer); await reactiveUtils.whenOnce(() => !layerView.updating); // A later call may have overtaken us during the awaits above. Anything that // touches the map or the panel from here down is gated on the ticket. if (ticket !== seq) return; // What the analyst sees: matches at full strength, the rest grayed but present. layerView.featureEffect = new FeatureEffect({ filter: new FeatureFilter({ where }), excludedEffect: "grayscale(100%) opacity(30%)" }); // What the analyst quotes: ask the side that can actually answer. const count = layerView.hasAllFeatures ? await layerView.queryFeatureCount({ where }) : await layer.queryFeatureCount({ where }); if (ticket === seq) panel.showCount(count); }
Four decisions in that construction earn their keep. outFields is
set on the layer at construction, so the fields the filter reads are in
availableFields by the time the layer view settles. The
whenOnce guard is there because both
hasAllFeatures and the query results are meaningless until the layer
view has stopped fetching. The effect deliberately removes nothing, so the analyst can still see
the blocks the matches sit in. And the ticket comparison is the
difference between a panel that shows the latest answer and one that shows whichever answer
arrived last. On a criteria panel the user is actively adjusting, those are not the same thing,
and the second is the exact defect this article exists to prevent.
One note on that ternary. It is an optimization, not a correctness mechanism — both branches are documented to return the same answer, and the flag only decides whether you pay for a round trip. If you would rather not depend on an SDK-internal decision at all, delete it and always ask the layer. You lose a few milliseconds and gain one less thing to re-verify after an upgrade.
What that panel still owes you
The snippet above is the shape, not the shipping version. Three things separate them.
Cost. Every criteria change that takes the service branch is a round trip.
Debounce the handler and memoize on the inputs that determine the answer — the
where clause, plus the extent if the query is extent-scoped. A count
is one of the most cacheable things an application asks for and one of the most frequently
re-requested. On a rate-limited service, an unthrottled criteria panel is a throttling event
rather than merely a slow one. Cancellation is available too:
FeatureLayerView.queryFeatureCount() takes an
AbortOptions second argument, so a superseded request can be dropped
outright rather than ignored on arrival.
The list after the count. Every results panel that starts as a number
eventually becomes a list, then an export. That is where
maxRecordCount arrives — the guide is explicit that paginated
queries are required for more than the service’s maximum record count. A count is one
request; a list is a paging design. Do not let the count’s simplicity set expectations for
what follows it.
Staleness. This architecture reads the map from a client cache and the number
from the service, which is two sources that can disagree. On an editable operational layer the
panel can legitimately print a count for features the map has not drawn yet. And if the count
should follow the map as the user pans, watch the fetch cycle rather than the extent. The shape
below is the one the hasAllFeaturesInView reference uses in its own
example:
reactiveUtils.when(() => !layerView.dataUpdating, async () => { const target = layerView.hasAllFeaturesInView ? layerView : layer; panel.showCount(await target.queryFeatureCount({ where, geometry: view.extent })); });
The switch from updating in the first construction to
dataUpdating here is deliberate.
updating is true whenever the layer view is busy, including
re-rendering after an effect changes. dataUpdating is the narrower
signal that features are being fetched, which is what a count depends on.
Two caveats on that snippet, both worth knowing before you paste it.
when fires on a transition into truthy, so if the layer view has
already settled at registration time the callback waits for the next fetch cycle; pass
{ initial: true } if you need the first render populated. And the
dataUpdating reference itself recommends the stricter pattern of
watching updating and dataUpdating
together with a latch, because dataUpdating can only be true while
updating is. Use the simple form for a panel; use the latch when you
must be certain a full update cycle completed.
Display filters, the fourth lever
The flag table above names a display filter without defining it, and it deserves the paragraph, because at 5.1 it is often the right answer to the dense-map problem behind the parcel example.
DisplayFilterInfo, added at 4.32, controls which features are
drawn. The reference’s own summary is that it lets you “display a subset of features
while retaining access to all features for querying and analysis.” Unlike
definitionExpression, which “filters data at the source
level,” display filters “only affect visibility on the map.” Its canonical use
is scale-dependent
decluttering: major rivers when zoomed out, tributaries as you zoom in. On a
two-hundred-thousand-parcel layer that is the difference between a map that is usable at county
extent and one that is not — and the featureEffect approach,
which keeps every non-match on the map, is the option that needs it most.
It also closes the loop on the flag. The same reference explains that display filters
“may be appended to the layer’s definitionExpression when
querying the service.” The consequence, in its words: “the filtered features may not
be available on the client for executing layer view queries.” It then names the check, the
one
recommended above: read hasAllFeaturesInView once
dataUpdating is false, and query the layer if the flag is false. The
reference’s own warning is this article’s thesis restated: display filters
“should be ignored when querying data to present to users.”
Five places a filter or an effect quietly does nothing
Each of these is documented, and each produces the same symptom: correct-looking code and no visible change.
- A feature effect in a 3D scene. The reference lists
SceneViewamong the scenariosFeatureEffectdoes not support. Layer-wideeffectis unsupported in 3D as well. A 2D prototype that ports to a scene loses both silently. - A feature effect on a clustered layer. With
FeatureReductionClusterenabled, feature effect is not supported — and neither is layereffect. The features you are trying to emphasize are not drawn as features any more — so if clustering is the answer to a slow map, a display filter or a scale threshold becomes the tool for context instead. - A printed map. The layer view reference lists
“when a map is printed” among the scenarios feature effect does not support. The
layer-wide
effecthas its own printing constraints, which are conditional rather than blanket. If a report export is part of the deliverable, verify the printed output early rather than at handover. - An effect that had to survive a save. Covered in the callout above: layer
view effects never persist to a web map, and layer effects persist only with
excludedLabelsVisibleset true and an attribute-only filter. This one works perfectly until somebody saves the map. - A where clause on a field nobody fetched. As a filter this fails
silently, because there is nothing wrong with the clause — there is just nothing to
evaluate it against. As a query it is less forgiving: the reference warns that a query
on a field outside
availableFieldsmay fail outright or return incomplete results.
Grayed out is not filtered out — the accessibility half
The argument for featureEffect over removal is that context is
information. That argument has an obligation attached, and it is the one most often skipped.
grayscale(100%) opacity(30%) is the excluded effect in most Esri
samples and in this article. At 30 percent opacity over a light basemap it will often fall below
the 3:1 non-text contrast floor WCAG sets for meaningful graphics. It also removes color as a
cue, which is the right instinct for a color-blind reader only if something else carries the
distinction. Measure the excluded state against your actual basemap rather than trusting the
sample, and prefer an included effect that adds emphasis over an excluded effect that subtracts
everything.
The deeper point is that map features are drawn to a canvas, so neither the grayed features nor the hidden ones exist in the accessibility tree. For a screen reader user the distinction this section is built on does not exist, and the results panel is the map. That makes the count more important, not less — announce it in a live region so a change of criteria is perceivable without sight of the map.
Choosing, in one line each
- Must never reach the user — a server-enforced mechanism: a hosted
feature layer view with a view definition, an Enterprise database view, or a proxy.
definitionExpressionreduces payload; it does not enforce anything. - Must respond instantly to a control the user is holding —
FeatureFilteron the layer view, over fields you named inoutFields. - Must keep the non-matches on screen for context —
FeatureEffect, withexcludedEffectrather than removal, and a contrast check on the result. - Must declutter a dense map without hiding data from queries — a
display filter, and then
hasAllFeaturesInViewbefore you trust a client-side count. - Must produce a number somebody will act on — query the layer,
debounce it, and reject superseded responses.
hasAllFeaturescan skip the round trip when it is true; it changes the latency, not the answer. - Must produce a measurement — ask the service for the statistic with
outStatisticsrather than downloading geometry to sum it in the browser.hasFullGeometriesis the flag that tells you client-side measurement was never on the table.
References
- Introduction to query and filter (the server-side vs client-side layer split, and the guidance table this article follows)
- FeatureFilter reference (the seven properties, and the accuracy warning about features available for drawing)
- FeatureEffect reference (includedEffect, excludedEffect, the unsupported scenarios and the web map persistence rules)
- FeatureLayerView reference (filter, featureEffect, availableFields, and the three 4.29 trust flags)
- FeatureLikeLayerView reference (the query method notes on case sensitivity, quantized geometry and availableFields)
- FeatureLayer reference (definitionExpression, effect, queryFeatureCount and the property application order)
- DisplayFilterInfo reference (the fourth lever, and the documented reason hasAllFeaturesInView goes false)
- Set a view definition on a hosted feature layer view (the server-enforced alternative when a restriction is a requirement rather than a convenience)
- Sample: filter features by attribute (a running construction of a layer view filter driven by a control)
- Sample: apply effects to features (a geometry-driven feature effect with an excludedEffect)
- Release notes for 5.1 (the current release this article was verified against)
Is the number in your map application defensible?
We review ArcGIS Maps SDK applications for exactly this class of defect — counts read from the wrong side of the pipeline, filters evaluated against fields nobody fetched, and restrictions implemented as visibility rather than as policy. Book a free intro call and we will talk through what a review would find on your stack.
Book a free intro call