Log Entry

"Props, Events, LMS, or State Manager?"

Jun 24, 2026 · 14 min read

LWC has several ways to move information between components.

That does not mean they are interchangeable.

When a component uses Lightning Message Service to tell its own child that a button was clicked, the communication mechanism is hiding a relationship that the template already knows.

When data is passed through eight components that do not use it, plain properties are doing too much.

The mechanism should describe the relationship.

Quick snapshot

6 / 6 rows

Owner sends data to childPublic property
Owner commands a specific childPublic method, sparingly
Child reports an occurrence to ownerCustom event
Components in different page regions communicateLightning Message Service
Related descendants share complex reactive stateState manager
Several components need the same server record dataLDS-backed adapters or state manager

My default is still the simplest pair:

Properties down. Events up.

Salesforce's component event guidance describes the same basic direction: properties or public methods down the containment hierarchy, events up, and Lightning Message Service when components are not in a direct relationship.

Properties make ownership visible

A parent owns data and passes the required part to a child.

<c-order-summary
  order={order}
  currency-code={currencyCode}>
</c-order-summary>

That is easy to understand from the template. It also keeps the child portable: the child knows its input, not where the parent obtained it.

Treat objects received through public properties as read-only. If the child needs editable local state, copy the values it owns rather than mutating its parent's object.

I prefer narrow public properties over one enormous config object unless the configuration genuinely has one stable shape. A catch-all object often turns into an undocumented dependency bag.

Public methods are commands, not data flow

A public method can be useful when an owner needs to ask a specific child to perform a UI action:

  • focus an input
  • reset a form
  • open a panel
  • run validation
@api
focusSearch() {
  this.template.querySelector('lightning-input')?.focus();
}

I avoid using public methods as a back door for continuous state synchronisation. If the owner repeatedly calls setRows(), setFilter(), and setSelection(), those values probably belong in properties or shared state.

A method says "do this now". A property says "this is the current input".

Events report what happened

A child should not reach up and manipulate its owner.

It should report an occurrence.

this.dispatchEvent(
  new CustomEvent('quantitychange', {
    detail: {
      lineId: this.lineId,
      quantity: this.quantity
    }
  })
);

The event name should describe the occurrence, not prescribe the parent's implementation.

quantitychange is better than updateordertotal. The child knows that its quantity changed. It should not decide that the owner must update a total, write a record, show a toast, or do all three.

Be conservative with bubbles and composed. A widely propagating event becomes part of a broader public API and can create collisions or surprising listeners. Start with the narrowest propagation that fits the component boundary.

Use LMS for page-level distance

Lightning Message Service is for components that need to communicate without a direct DOM ownership relationship.

Typical examples include:

  • a utility-bar component reacting to a record selected in a workspace tab
  • an Aura component and an LWC sharing page context
  • a Visualforce surface communicating with Lightning components
  • independent page regions responding to the same selection

That is real distance.

LMS is not a universal event bus for every click in the application.

Messages have a Lightning Message Channel contract. Keep the payload small and stable:

publish(this.messageContext, RECORD_SELECTED, {
  recordId: this.recordId,
  source: 'alarm-list'
});

The default subscription scope is the active area. Use APPLICATION_SCOPE only when the message genuinely needs to cross the whole application; Salesforce documents the distinction in LMS subscription scope.

Global scope is not a convenience flag. It increases the number of places that may react.

Use a state manager for shared, coherent state

State managers solve a different problem.

They are useful when a collection of related components needs shared reactive state, derived values, and actions:

  • a shopping cart with lines, totals, discounts, and inventory state
  • a complex workspace with filters, selection, results, and commands
  • a multi-panel editor with unsaved local changes

Properties and events can still model this, but deep prop drilling and event relaying can overwhelm the actual UI structure.

A state manager gives the subtree one coherent state model. Salesforce's state-management comparison positions it for applications or component groups that have grown large and complex, while noting that properties and events remain optimal for many smaller relationships.

I keep the state manager about state:

  • atoms hold mutable values
  • computed values derive state
  • actions make allowed changes
  • components own DOM interactions

Salesforce explicitly advises that state managers should not depend on DOM elements or events. That separation keeps the state layer testable and the UI replaceable.

State manager or LMS?

This is the easy distinction to lose.

Use a state manager when the components are part of one composed application or subtree and need shared state with consistent actions.

Use LMS when separately composed surfaces need to exchange a message across page or technology boundaries.

A message says something happened.

A state manager says this is the current coherent state.

Turning LMS into a state store creates ordering and replay questions. Turning a state manager into a global application singleton can couple unrelated pages forever.

Do not fetch the same data in every leaf

A shared record displayed by several components does not automatically require custom shared state.

LDS already provides a client-side data layer. Several components can use LDS-backed adapters and benefit from its cache and update notifications.

Use a state manager when the application needs coordinated local state, derived values, or a cohesive action model beyond simply reading the same cached record.

My selection test

I ask four questions:

  1. Do these components have a direct owner-child relationship?
  2. Is this an input, an occurrence, a command, or persistent shared state?
  3. Does the information need to cross a page-composition boundary?
  4. Who is responsible for keeping the value coherent?

The answers usually point to one mechanism.

If they point to all four, the component boundaries probably need another look.

Communication code should make the architecture easier to see.

That is the standard I use.