Limiting States
Photo by Hans-Peter Gauster
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: boolean3}45function Toggle({isLoggedIn}) {6 const [on, setOn] = React.useState<boolean>(false)78 function toggleIfLoggedIn() {9 if (isLoggedIn) {10 setOn(on => !on)11 }12 }1314 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.
count | isLoggedIn | on |
---|---|---|
1 | false | false |
2 | false | true |
3 | true | false |
4 | true | true |
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: boolean3}45function 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)1011 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 }, [])2526 function toggleIfLoggedIn() {27 if (isLoggedIn) {28 setOn(on => !on)29 }30 }3132 if (isLoading) return <span>Loading...</span>3334 if (hasError) return <span>{error}</span>3536 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:
count | isLoggedIn | on | error | isLoading | hasError |
---|---|---|---|---|---|
1 | false | false | null | false | false |
2 | false | false | null | false | true |
3 | false | false | null | true | false |
4 | false | false | null | true | true |
5 | false | false | string | false | false |
6 | false | false | string | true | false |
7 | false | false | string | true | true |
8 | false | false | string | false | true |
9 | false | true | null | false | false |
10 | false | true | null | false | true |
11 | false | true | null | true | false |
12 | false | true | null | true | true |
13 | false | true | string | false | false |
14 | false | true | string | true | false |
15 | false | true | string | true | true |
16 | false | true | string | false | true |
17 | true | false | null | false | false |
18 | true | false | null | false | true |
19 | true | false | null | true | false |
20 | true | false | null | true | true |
21 | true | false | string | false | false |
22 | true | false | string | true | false |
23 | true | false | string | true | true |
24 | true | false | string | false | true |
25 | true | true | null | false | false |
26 | true | true | null | false | true |
27 | true | true | null | true | false |
28 | true | true | null | true | true |
29 | true | true | string | false | false |
30 | true | true | string | true | false |
31 | true | true | string | true | true |
32 | true | true | string | false | true |
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: false3}45interface Rejected {6 isAuthenticated: true7 hasError: true8 error: string9}1011interface Pending {12 isAuthenticated: true13 isLoading: true14}1516interface Success {17 isAuthenticated: true18 on: boolean19}2021type ToggleState = Unauthenticated | Success | Rejected | Pending
Which would result in the following possible states:
count | isLoggedIn | on | error | isLoading | hasError |
---|---|---|---|---|---|
1 | false | N/A | N/A | N/A | N/A |
2 | true | true | N/A | N/A | N/A |
3 | true | false | N/A | N/A | N/A |
4 | true | N/A | string | N/A | true |
5 | true | N/A | N/A | true | N/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.