This site runs best with JavaScript enabled.

Limiting States


How to limit states through data modeling.

Limiting State

State management is hard. This major reason for this is the effects of combinitorial explosion. In this article I want explore how we can limit the effects of combinitorial explosion thus making state easier to manage and our apps simpler.

Modeling State

Most developers spend little time thinking about the possible ways to model their state. Whatever gets the job done is fine. Right? Lets look at some ways we could model our state. Lets say we have a toggle component. One implementation might look like this:

1function Toggle() {
2 const [on, setOn] = React.useState<boolean>(false)
3 return (
4 <label aria-label="Toggle">
5 <input type="checkbox" checked={on} onClick={() => setOn(on => !on)} />
6 </label>
7 )
8}

This is a pretty simple component that a boolean as state probably works fine for. But lets suppose our product manager comes to us and says "We only want the user to be able to toggle when they are logged in". Now our component would look something like:

1interface ToggleProps {
2 isLoggedIn: boolean
3}
4
5function Toggle({isLoggedIn}) {
6 const [on, setOn] = React.useState<boolean>(false)
7
8 function toggleIfLoggedIn() {
9 if (isLoggedIn) {
10 setOn(on => !on)
11 }
12 }
13
14 return (
15 <label aria-label="Toggle">
16 <input type="checkbox" checked={on} onClick={toggleIfLoggedIn} />
17 </label>
18 )
19}

Now we have two pieces of state (props are just passed in state). So the total possible states we can be in is now 4.

countisLoggedInon
1falsefalse
2falsetrue
3truefalse
4truetrue

Now lets say that our product manager comes back to us and says: "You know what lets save whether our toggle is on or off to the database". So we make that change.

1interface ToggleProps {
2 isLoggedIn: boolean
3}
4
5function Toggle({isLoggedIn}) {
6 const [on, setOn] = React.useState<boolean>(false)
7 const [error, setError] = React.useState<string>(null)
8 const [isLoading, setLoading] = React.useState<boolean>(false)
9 const [hasError, setHasError] = React.useState<boolean>(false)
10
11 React.useEffect(() => {
12 setLoading(true)
13 fetch('apiResource')
14 .then(res => res.json())
15 .then(data => {
16 setLoading(false)
17 setOn(data.on)
18 })
19 .catch(errorResponse => {
20 setLoading(false)
21 setHasError(true)
22 setError(errorResponse)
23 })
24 }, [])
25
26 function toggleIfLoggedIn() {
27 if (isLoggedIn) {
28 setOn(on => !on)
29 }
30 }
31
32 if (isLoading) return <span>Loading...</span>
33
34 if (hasError) return <span>{error}</span>
35
36 return (
37 <label aria-label="Toggle">
38 <input type="checkbox" checked={on} onClick={toggleIfLoggedIn} />
39 </label>
40 )
41}

now we're getting the explosion of states we were talking about. Here is the total set of possible states now:

countisLoggedInonerrorisLoadinghasError
1falsefalsenullfalsefalse
2falsefalsenullfalsetrue
3falsefalsenulltruefalse
4falsefalsenulltruetrue
5falsefalsestringfalsefalse
6falsefalsestringtruefalse
7falsefalsestringtruetrue
8falsefalsestringfalsetrue
9falsetruenullfalsefalse
10falsetruenullfalsetrue
11falsetruenulltruefalse
12falsetruenulltruetrue
13falsetruestringfalsefalse
14falsetruestringtruefalse
15falsetruestringtruetrue
16falsetruestringfalsetrue
17truefalsenullfalsefalse
18truefalsenullfalsetrue
19truefalsenulltruefalse
20truefalsenulltruetrue
21truefalsestringfalsefalse
22truefalsestringtruefalse
23truefalsestringtruetrue
24truefalsestringfalsetrue
25truetruenullfalsefalse
26truetruenullfalsetrue
27truetruenulltruefalse
28truetruenulltruetrue
29truetruestringfalsefalse
30truetruestringtruefalse
31truetruestringtruetrue
32truetruestringfalsetrue

Yep. 32 possible states for 5 pieces of state. And this is for state that only has two possible values for each state. We've all seen states that have significantly more possible values for each piece of state and significantly more pieces of state. Consider if any one of these was a complex object that has properties that are nullable. Things get out of hand really quick if you're not careful in front end development.

A Better Way to Model Your State

looking at the chart above and thinking about what should be possible can get us a long way in getting to a better data model. For example if we model our state in such a way that if the user is not logged in then we can't have any of these other states that reduces our possible states in half. Further should it be possible to have both an error and loading at the same time? Probably not. How about having an error message but hasError be false. Turns out that also doesn't seem reasonable. What if we changed our model to only allow valid states? What would that do to our possible states?

Lets look at another data model to explore this idea. Imagine we had the following.

1interface Unauthenticated {
2 isAuthenticated: false
3}
4
5interface Rejected {
6 isAuthenticated: true
7 hasError: true
8 error: string
9}
10
11interface Pending {
12 isAuthenticated: true
13 isLoading: true
14}
15
16interface Success {
17 isAuthenticated: true
18 on: boolean
19}
20
21type ToggleState = Unauthenticated | Success | Rejected | Pending

Which would result in the following possible states:

countisLoggedInonerrorisLoadinghasError
1falseN/AN/AN/AN/A
2truetrueN/AN/AN/A
3truefalseN/AN/AN/A
4trueN/AstringN/Atrue
5trueN/AN/AtrueN/A

and thats it! 32 possible states becomes just 5 possible states! Thats less than 1/6th of the possibilities. This is much simpler to comprehend and implement and because we've modeled it in typescript the compiler will catch any places where we are doing something that our business logic says shouldn't be possible. This will result in much simpler and maintainable applications.

Conclusion

By modeling our state correctly we are able to limit the amount of possibilities we have to hold in our heads and consider and we get better TypeScript support. This helps us to eliminate bugs while simplifying the business logic so its both easier to understand and contains less surface area for bugs to creep into our programs.

Discuss on TwitterEdit post on GitHub

Share article
Tyler Haas

Tyler Haas is a full stack software engineer and primarily focusing on the front end. He has worked with companies of all sizes to help them deliver robust applications. He lives with his wife and three kids in Utah.