DEV Community

Tyler Haas
Tyler Haas

Posted on • Updated on

Limiting States

This is a cross post from my blog

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:

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

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:

interface ToggleProps {
  isLoggedIn: boolean
}

function Toggle({isLoggedIn}) {
  const [on, setOn] = React.useState<boolean>(false)

  function toggleIfLoggedIn() {
    if (isLoggedIn) {
      setOn(on => !on)
    }
  }

  return (
    <label aria-label="Toggle">
      <input type="checkbox" checked={on} onClick={toggleIfLoggedIn} />
    </label>
  )
}

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.

interface ToggleProps {
  isLoggedIn: boolean
}

function Toggle({isLoggedIn}) {
  const [on, setOn] = React.useState<boolean>(false)
  const [error, setError] = React.useState<string>(null)
  const [isLoading, setLoading] = React.useState<boolean>(false)
  const [hasError, setHasError] = React.useState<boolean>(false)

  React.useEffect(() => {
    setLoading(true)
    fetch('apiResource')
      .then(res => res.json())
      .then(data => {
        setLoading(false)
        setOn(data.on)
      })
      .catch(errorResponse => {
        setLoading(false)
        setHasError(true)
        setError(errorResponse)
      })
  }, [])

  function toggleIfLoggedIn() {
    if (isLoggedIn) {
      setOn(on => !on)
    }
  }

  if (isLoading) return <span>Loading...</span>

  if (hasError) return <span>{error}</span>

  return (
    <label aria-label="Toggle">
      <input type="checkbox" checked={on} onClick={toggleIfLoggedIn} />
    </label>
  )
}

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.

interface Unauthenticated {
  isAuthenticated: false
}

interface Rejected {
  isAuthenticated: true
  hasError: true
  error: string
}

interface Pending {
  isAuthenticated: true
  isLoading: true
}

interface Success {
  isAuthenticated: true
  on: boolean
}

type 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.

Top comments (0)