Learn where to keep your variables to improve your app design, performance, and readability.
What is State?
In programming, state refers to all the variables within a program. Variables represent data held in memory.
Global state refers to variables that are globally scoped. They can be accessed from anywhere within the entire app.
Local state refers to variables that are locally scoped. They can only be accessed within the file, component, or function where they are declared.
Derived state refers to variables that are calculated based on other variables.
const person = { firstName: "Paul", lastName: "Posey", id: 12 };
// displayName is derived state
const displayName = person.lastName + ", " + person.firstName;
When people talk about state in a front-end web app, they are typically referring to reactive state. Reactive state tracks updates and triggers effects when they happen. Reactive state management tools exist in all frameworks. Within them, a state variable is essentially a set of getters and setters. When you access the value, you're getting it. When you reassign the value, you're setting it. Tools that create reactive state will also have methods for instantiating the state variable, updating it, accessing its previous value, and more.
Local State Management
When you are creating a local state variable, as yourself:
- Does this variable need to be updated repeatedly?
- Do updates to this variable need to trigger updates somewhere else?
If not, a regular let
or const
should suffice. Reactive state management tools are more powerful and will consume more resources. Why maintain a Ferrari when a bicycle will suit your needs?
If you do need to update the variable and trigger effects based on updates, you'll want to create reactive state.
In all three frameworks, you pass an initial value to a method. When you change the value, effects will be triggered. Anything that uses these reactive state variables will be notified when there's an update. So if you're displaying the value of a reactive state variable, the new value will be displayed.
Frameworks also provide ways to trigger complex effects after an update. For example, a user clicks the "next page" button, so the value of your pageNumber
state variable is incremented, and you need to make an API call to get the next page of results. You'd use useReducer or useEffect in React, watchers in Vue, and effects in Angular.
In Angular, you can create reactive state using signal
.
const person = signal({ firstName: "Paul", lastName: "Posey", id: 12 });
If you just want to assign a new value, you can use set()
to update your signal. If you want to compute a new value based on the old value, you use update()
.
In Vue, you can create reactive state using ref
.
const person = ref({ firstName: "Paul", lastName: "Posey", id: 12 });
A ref maintains a reference to the original value and creates a Proxy. You access and reassign the value within the Proxy using .value
.
if (person.value.firstName === "Paul") {
person.value = { firstName: "Pauline", lastName: "Person", id: 14};
}
In React, you create a state value and an update function using the useState
hook. (A hook is a function that let you “hook into” React features within a functional component.)
const [person, setPerson] = useState({ firstName: "Paul", lastName: "Posey", id: 12 });
Derived State Management
- React:
const
- Vue: computed
- Angular: computed signals
When possible, keep derived state in the least powerful state tool.
In React, it is more performative to declare derived state with const
. An update to a reactive state variable will trigger a component re-render. If you update one variable declared with useState
and trigger an update to another variable declared with useState
, the component will re-render twice. If you just use const
, the value will re-compute during the first re-render anyway.
// Triggers a second re-render
const [person, setPerson] = useState({ firstName: "Paul", lastName: "Posey", id: 12 });
const [displayName, setDisplayName] = useState(() => person.firstName + " " + person.lastName);
// Doesn't trigger a second re-render
const [person, setPerson] = useState({ firstName: "Paul", lastName: "Posey", id: 12 });
const displayName = person.firstName + " " + person.lastName;
In Vue and Angular, this is when you'd use computed()
.
const displayName = computed(() => {
return person.value.lastName + ", " + person.value.firstName;
});
Put simply, whenever person
is updated, displayName
will update.
Vue and Angular use the observer pattern and keep track of which reactive variables trigger which effects. Instead of re-rendering the entire component, only the things that need to update will update.
In observer pattern terms, the reactive variable person
is the subject. When we use person
in the callback function passed to computed()
, the function is added to a list of observers. When the subject updates, the observers are notified, and the function will run again. This should sound familiar - addEventListener()
also uses the observer pattern. With addEventListener()
, the subject is an event and your listener (the event handler callback function you pass) is the observer/effect.
Dependency Injection
- React: Context
- Vue: Provide/Inject
- Angular: Dependency Injection
The more complex your application gets, the more prop-drilling becomes a problem. In a small application, it's fine to pass state down to child or even grandchild components. As a rule of thumb, if you have to pass state as a prop to a great-grandchild component, it's time to re-evaluate the design. It's common to run into this problem when multiple components need to use the same data from an API call.
Before you reach for global state, consider an in-between solution - dependency injection. It's still local state, but instead of using props, parent components provide the state, and their children can consume it. That's why "dependency injection" and "provider/consumer" are used interchangeably.
This pattern isn't just for reactive state. If you have derived state or a complex calculation, you can use dependency injection as a type of memoization. You only have to call an expensive function once and then you can inject the result.
In Vue, a component uses the provide method to make a key value pair. Then, any child component can use the inject method to access that value.
// Parent component
const isLoggedIn = ref(false);
provide("key", isLoggedIn);
// Child component
const isLoggedIn = inject("key");
if (isLoggedIn) showData();
You can inject update functions into consumer components. To avoid unintended side effects, updates should stay in the provider component.
Angular also has an inject method for dependency injection. It provides multiple ways to create a provider. A common pattern is creating a class service, but you can also use values like a string, boolean, or Date. If you need to trigger updates from a consumer, your service class can define getters and setters.
Dependency injection in React involves four hooks - useReducer
, createContext
, createContextDispatch
, and useContext
. Writing a context requires understanding concepts that are used in global state management libraries. I'll explain how to write one in the next part of this series.
Basically, createContext
creates a component to hold your state and createContextDispatch
creates a component with methods for updating your state.
To use a context you've written, you need to wrap your components in a provider tag.
<PersonContext.Provider value={person}>
<PersonDispatchContext.Provider value={dispatch}>
<WelcomeBanner />
<MainContent />
<Footer />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
These days (v19.1), you no longer have to use .Provider
when using your context provider component.
<PersonContext value={person}>
<PersonDispatchContext value={dispatch}>
<WelcomeBanner />
<MainContent />
<Footer />
</TasksDispatchContext>
</TasksContext>
Consumer child components use the useContext()
hook to access the state. Before the useContext()
hook was added, you had to use a consumer component to access your context.
return (
<PersonDispatch.Consumer>
{(person) => (
<p>{ person.firstName }</p>
)}
</PersonDispatch.Consumer>
)
Global State Management
What if you need to consume the same data and trigger the same API call to update that data in two unrelated components? It's common to run into this problem with auth (e.g. checking if the user has logged in). Global state is for variables that are used and updated in multiple unrelated files.
Technically, you could make the argument that putting variables and update functions in your main file and using prop-drilling is global state management.
When we're talking about front-end web development, global state management usually refers to a library that makes your data available without the parent/child relationship required to prop-drill or use dependency injection. You should only use a global state management library when it's worth the performance trade-off.
A global state management library typically creates a store. This pattern is so ubiquitous that sometimes global state management libraries refer to themselves as "a store." You store your state in a store. A store can maintain complex data structures. A store provides a standard way to make changes to your global state, making state changes predictable.
There are a couple common patterns for writing stores and triggering updates in global state management libraries. I'll cover them in the next two parts of this series (coming June or July 2025).
- State Management in Front-end Web Development: Actions, Dispatch, and Reducers
- State Management in Front-end Web Development: Mutators
Top comments (20)
State Management, and reactive state, can be tricky. And yes, with more complex cases (nested components, messaging between endpoints etc), packaged solutions can be useful.
But we can quite easily emulate things like that in a few ways - if we borrow a page from Lit.js playbook, we can use custom events to create a Context system within our HTML, if we want a simpler solution I've written dev.to/parenttobias/a-simple-obser... about building a simple Observable that can handle side effects (and derived State).
To your excellent point, I have updated the article to make it clear that Vue and Angular use the observer pattern, like event listeners. 😁
I would love to see a codepen or repo with the example from your article all together. It took me a hot minute to parse it myself, and this series is aimed at beginners. I think being able to reference it all together would help people follow along. It reads like eavesdropping on two senior developers. 💙
Yours is by far the most pleasant comment about this, so I'll address it here. Yes, you do not have to use a framework or library for reactive state or state management. Yes, you can build your own. Yes, it would behoove people to understand CSS, HTML, and JS and how frameworks use them.
However, the reality is a lot of people, including me, were thrown into reactive state and state management in frameworks without being taught the underlying concepts. That is the target audience. The series started as just a library-agnostic explanation of actions, dispatch, and reducers, because I couldn't find that anywhere.
I may add a fourth part to the series about writing your own state management, but comments railing against framework use in general and shaming people for not already understanding the underlying concepts will be hidden.
Please understand, I don't rail against frameworks. I find them practical and useful. But I really like the idea of devs gaining insight into how they do their "magic."
Thank you for your time and consideration, I'll get a codepen together to demonstrate them.
My apologies - you certainly did not rail against frameworks. I happened to get the notification about your comment in a batch of notifications. Some of the other comments were, uh, ranty to say the least.
Excellent breakdown! The React examples really clarify when to use useState vs const for derived state - that performance insight about avoiding double re-renders is gold.
Your progression from local → dependency injection → global state perfectly captures the decision tree most React developers face. The Context API section especially hits home - it's powerful but definitely has complexity overhead that beginners might not expect.
I recently wrote about an approach that tries to bridge that gap - keeping React state simple with atomic updates while avoiding the provider patterns that can get unwieldy. Your point about "worth the performance trade-off" is exactly the balance we're all trying to strike.
Can't wait for the actions/dispatch/reducers deep dive in part 2! The way you explain these concepts makes them so much more approachable.
Really appreciate how you break down when to reach for derived vs reactive state - makes those trade-offs way clearer for me.
Any sneak peeks at what you'll cover in the next parts?
Part 2
Done:
Todo:
Part 3
Maybe a part 4
This post is pure gold!
Love how clearly you explain this ...helpful for devs at all levels..
This is the kind of explanation I wish I had when I first started messing with state - makes stuff way less confusing honestly.
Never seen state described in such straightforward terms. Thanks for the lesson, this is incredibly useful!
Usually, you don't even need a fat library either. Usually, you can a) do all you need with a basic message bus or, even better, you can b) literally write your own reactive/redux-style Store very easily that scales from a—z to infinity. It is literally just 20 lines for a classical Subject (see The Observer Pattern, GoF), about 5 lines for a Store base class, and 5 lines for a base Reducer class; you can even override the main method for get/set functionality in Web Storage (+ 3 lines). The sky is the limit from there.
BTW, you don't need an L10n library either. It's like 8 lines to create that, too, even if you want to rig up The Chain of Responsibility handlers for doing advanced things like interpolation or swapping entire markdown files (with interpolations) for [example] large Terms of Agreement texts and so on. The sky is the limit from there.
You don't need a library, you need a senior. Senior Engineers keep scaling, most libraries do not.
growth like this is always nice to see. kinda makes me wonder - what keeps stuff going long-term? like, beyond just the early hype?
Nice that you wrote an article about state management! State is the evil ☠️, so it's always good to talk about it.
I'm not sure I agree with "In programming, state refers to all the variables within a program. Variables represent data held in memory."
In web applications, "state" refers to the data that represents the current condition or status of the application at any given moment.
Derived state, is not state 😄 Why? Because it can be derived from one of the variables that are needed and enough to describe the status of the app.
I'm not sure everybody thinks this: "When people talk about state in a front-end web app, they are typically referring to reactive state.". Reactive is a property of the state, applied by a pattern that introduces the abstraction, and not the state per se. React doesn't use the reactive pattern, unless you go for it with MobX, etc..., or any package that implement the observer pattern.
Anyways, glad you wrote about it! Teaching is also a learning experience, and from the discussion we both can learn! Leaving you a reading suggestion that I believe will enhance a bit your mental model on state: manning.com/books/grokking-simplicity
Thanks and keep writing! 🙏
I'm not sure I agree with “State is the evil ☠️”. State is a tool. Like any other tool, it can be misused.
You’re not wrong - state is the current status/data in a web application. And that’s held in variables. In the context of state management and when not to use reactive state for beginners, aka State 101, I’m creating an approachable mental model of “regular variables” vs “fancy/reactive variables”. You could argue it’s more of an abstraction or less of an abstraction than your framing depending on how you look at it.
I’m not sure I agree with “Derived state, is not state 😄…Because it can be derived from one of the variables that are needed and enough to describe the status of the app.” By both of our definitions, it is included under the umbrella of state. I haven’t seen anyone else make that distinction, and it seems a tad pedantic. 😄 Regardless, derived state is important to discuss in this context because I see even experienced developers putting derived state in reactive state.
You’re not wrong - I find most people don’t think the way I do. I say typically because in my experience talking to other developers, most people use “state” as a shorthand for “state that is stored in a variable with a reactive property applied to it by a pattern that introduces the abstraction, and not the state per se” regardless of the framework or reactive pattern. While you are correct that React doesn’t use the observer pattern and instead triggers a re-render and that re-render uses lifecycle events to trigger effects, I make that distinction in the derived state section. It’s still using a reactive pattern in that it reacts to changes in data (and we’re building that “regular variables” vs “fancy/reactive variables” mental model). You have hit on what I’m building towards with this series - reactive state variables are just variables with a reactive pattern applied. The only real difference between local state management and global state management is how the reactive pattern is applied and how you access the variables.
I’m glad you like the article! Hope you learned something from the discussion too. I agree teaching can be a great tool for learning. I also find that experts forget what it’s like to be a beginner. I recommend reading Why Experts Can Struggle to Teach and A Novice -> Expert Model of Learning.
Some comments have been hidden by the post's author - find out more