Jacob Kofoed
January 22, 2024
Developers think in states. They do so because you need to when you develop for the web. Each part of your app relies on different states, and the application can be viewed as a unique combination of all those different states at one point in time. State refers to the storage of data that reflects the condition or context of an application or component at a specific time. A single variable, for instance. Considering that developers think in states, it is incredibly important that we simplify how many states you have to juggle as you develop.
We will explore some types of states you can utilize when developing.
1. Local State
You use local state when you handle data that is only relevant within a specific part of your application, like toggling a dropdown menu or tracking input form values. In frameworks like react, vue.js, or toddle, we usually refer to data used and managed by a single component. In a toddle component, for instance, this would be a variable. Local state is the most common form of state, as it is perfectly encapsulated where it is used. So when you sit in some other part of your app, you don’t need to remember what goes on with this state; therefore, it scales better than some of the other types we will discuss.
2. Inherited State (Context)
An inherited state can be achieved when you borrow some state from another element. This can be through props/attributes & events or a more sophisticated measure such as a context system. Some modern web frameworks have a context feature built-in. In toddle, you can use context to use any value throughout your app. It exposes certain values from a component considered “global” within a particular tree of your components, such as which user is logged in, what accordion is currently expanded, feature flags, or a theme variable. Therefore, context fits somewhere between local and global states.
3. Global State
Global state refers to data accessible by multiple components across the entire application. It is typically used for data that needs to be shared widely like user authentication status or other API data you want to ensure is only fetched once. Managing a global state can be complex and traditionally involves using state management libraries as complexity grows just a little. In toddle, you can use either the URL Parameters (see 6.) or the context feature at a high level to expose to the entire subtree.
Why not use global state everywhere?
Global state seems great initially, but there are rarely good use cases. Using the global state too often can lead to an explosion in combined state complexity, leading to unpredictable state combinations and performance concerns. (In human speak: Don’t do it, you’ll regret it. Seriously, you’ll really regret it)
4. Local Storage
You can use local storage to store preferences, settings, or any data that should be kept across browser sessions. Data stored here is persistently in the browser, even when the browser is closed and reopened.
5. Session Storage
Session storage is great for data that should persist only as long as the session lasts, like data in a multi-step form on a single-page application. Session storage is similar to local storage, but it has a shorter lifespan. Data stored in session storage is cleared when the page session ends — that is when the browser tab is closed.
6. URL Parameters
URL parameters are used to maintain state in navigation, like the current page in pagination, or to store state across browser refreshes. URL parameters are dynamic values in the URL that can represent state. You can manipulate the URL parameters to maintain state without affecting the server or relying on local storage. URLs have been around for a long time, and they are still a great method for storing serialized states as they can be used to recreate this state across time and space. Just paste the URL, and you or that friend you gave the link to is back to where you were.
7. Server State
We usually access server state through APIs or Web Socket connections. The server state differs from the other states we've discussed, as you do not have direct access to the data. Instead, our job is to best sync the server's source of truth to the client. We have a few patterns to handle different use cases:
-
Invalidate APIs: After a request has been sent, we tell all our dependent client-side APIs that data may have changed. This comes with a small delay as we have to do a TCP from CLIENT -> SERVER -> CLIENT, but we won't have to write additional logic to keep the state on the server and client.
-
Optimistic update: Assume the request went fine and immediately update some state on the client side. This requires us to keep a copy of the state client-side.
-
Socket connection: If multiple users can modify the same data, we will need something that resembles live data, and the closest we can get is when we set up a Web Socket for dual-directional data. For even faster performance, you can combine a socket connection with optimistic updates.
Conclusion
If you want to build powerful web apps, you need to familiarize yourself with the various types of states. A good understanding of state is necessary to build highly performant and scalable apps. Each type of state has its use cases, and when you understand them, you can ensure seamless state management across your applications.
A note on complexity
A good tip is to use the lowest level of state pattern needed for each use case, as it can dramatically reduce your app's complexity. Each state variable you introduce will exponentially increase the number of possible states. It follows the formula 2^n for binary states. For example, adding a single extra global theme variable doubles (!) your app’s complexity in terms of possible states. This rapid growth makes it almost impossible to comprehensively test and predict every state combination, especially for edge cases. Encapsulating and controlling states is essential.
Learn how to build with Context here.