State Management in Front-end Web Development: State 101


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>
Consumer child components use the useContext()
hook to access the state. Before the useContext()
hook was added, you had to use a consumer tag like <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
Subscribe to my newsletter
Read articles from Abbey Perini directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Abbey Perini
Abbey Perini
💻 Full-Stack Developer 🧶 Fiber artist 🧘🏼 Yoga Teacher 🤓 Full-time nerd ...did someone say animated CSS button?