Ready to level up your LWC architecture? We explore the new defineState syntax, a powerful way to manage complex application state outside of your UI components using atoms and actions.
Index / Table of Contents
- Introduction
- Why Do We Need a State Manager?
- Core Concepts: Atoms and Actions
- Step 1: Creating the State Manager
- Step 2: Connecting to the Component
- Step 3: Building the UI
- Step 4: Sharing State (Context)
- Deep Dive: Understanding the Syntax
- Conclusion
Introduction
For years, LWC developers have relied on @track and @api to manage data within a component. It works great for simple, isolated widgets. But what happens when your application grows?
Salesforce has introduced a more robust pattern for state management using the @lwc/state module. This allows you to decouple your business logic (state) from your presentation logic (UI).
If you are familiar with React’s hooks or Redux, this will feel very powerful. Let’s dive into how to build a state manager using defineState.
Why Do We Need a State Manager?
Standard LWC state (using class properties) is tied to the component instance. If the component is destroyed, the state is lost.
By using defineState, you create a portable, reusable logic container. You can pass arguments to initialize it, and it exposes a clean public API (values and functions) that your component can use.
Core Concepts: Atoms and Actions
Before we code, let’s learn the vocabulary of @lwc/state:
- defineState: The main function that wraps your logic.
- atom: Represents a single piece of reactive data (like a counter number).
- setAtom: The only safe way to change an atom’s value.
- computed: A value that automatically updates when an atom changes (derived state).
Step 1: Creating the State Manager
Let’s build a Simple Counter. Instead of writing logic inside the LWC component, we will write it in a separate JavaScript file.
To make this shareable across multiple components, it’s best practice to place this file in a shared library component (e.g., c/smCounter).
Create a file named counterManager.js:
import { defineState } from "@lwc/state";
// defineState takes a callback function where we define our logic
const counterManager = defineState(
// The first argument provides the tools: atom, computed, setAtom
({ atom, computed, setAtom }, initialValue = 0) => {
// 1. Define State (The Atom)
// An 'atom' is a reactive wrapper around a value.
const count = atom(initialValue);
// 2. Define Actions
// Functions that modify the state.
const increment = () => {
// We use setAtom to update the value reactively
setAtom(count, count.value + 1);
};
// 3. Expose the Public API
// Only return what the component needs to know about.
return {
count,
increment,
};
},
);
export default counterManager;Step 2: Connecting to the Component
Now that our logic is defined, we can “hook” it into our visual component.
In simpleCounter.js, we initialize the manager. Notice how clean the component code becomes—it has no business logic, just connections!
import { LightningElement } from "lwc";
// Import from the shared component location
import counterManager from "c/smCounter";
export default class SimpleCounter extends LightningElement {
// Initialize the manager with a starting value of 100
counter = counterManager(100);
// Simple handler to trigger the action
handleIncrement() {
this.counter.value.increment();
}
}Step 3: Building the UI
<template>
<lightning-card title="Example: Simple Counter">
<!-- Display the reactive value -->
<section class="slds-p-around_small">
<p>Count: {counter.value.count}</p>
</section>
<!-- Trigger the action -->
<section class="slds-p-around_small">
<lightning-button
label="Increment"
onclick={handleIncrement}>
</lightning-button>
</section>
</lightning-card>
</template>The HTML template looks almost the same as standard LWC, but we access the data through our manager instance.
Step 4: Sharing State (Context)
One of the most powerful features of @lwc/state is the ability to share state with child components without passing props down manually (prop-drilling).
If SimpleCounter (from Step 2) initializes the state, any child component inside it can access that same state instance using fromContext.
The Child Component (Consumer):
import { LightningElement } from 'lwc';
import { fromContext } from '@lwc/state';
import counterManager from 'c/smCounter';
export default class CounterDisplay extends LightningElement {
// Look up the component tree for the nearest instance of counterManager
counter = fromContext(counterManager);
}The Child HTML:
<template>
<!-- This will display the SAME count as the parent -->
<p>Shared Count: {counter.value.count}</p>
</template>How it works: When you put <c-counter-display> inside the HTML of <c-simple-counter>, the child automatically finds the state initialized by the parent. If the parent increments the value, the child updates instantly.
Deep Dive: Understanding the Syntax
That code might look a bit advanced if you are new to functional programming patterns. Let’s break down the specific syntax rules.
The defineState Callback
defineState((...) => { ... })
The function you pass to defineState is your manager. It runs when you initialize the variable in your component (counter = counterManager(100)).
The Arguments
({ atom, computed, setAtom }, initialValue = 0)
- First Argument: This object is automatically injected by LWC. It gives you the “primitives” (tools) you need to build state.
- Second Argument (and beyond): These are your custom parameters. We used
initialValue, but you could pass config objects, IDs, or anything else.
Why setAtom?
You might be tempted to write count.value = 5. Don’t do that. Direct assignment won’t trigger the LWC reactivity system. By using setAtom(count, 5), you ensure that the UI knows the data has changed and needs to re-render.

Find salesforce documentation for better understanding: https://developer.salesforce.com/docs/platform/lwc/guide/state-management.html
Conclusion
Moving to @lwc/state and defineState requires a mindset shift. You are moving away from “monolithic” components where UI and Logic are mixed, toward a cleaner architecture where State Managers handle the data and Components just display it.
Start small. Try refactoring a simple form or counter to use this pattern. Once you see how easy it is to reuse that logic in a second component, you won’t want to go back!
