DEV Community

loading...
Cover image for The state you've never needed

The state you've never needed

macsikora profile image Maciej Sikora ・5 min read

Every application has a state. State represents our application data and changes over time. Wikipedia describes state as:

A computer program stores data in variables, which represent storage locations in the computer's memory. The contents of these memory locations, at any given point in the program's execution, is called the program's state

And the most important part of this quote is "at any given point", what means that state changes over time. And that is the reason why managing state is one of the hardest thing we do. If you don't believe me, then remind yourself how often you needed to restart computer, tv or phone when it hangs or behave in strange way. That exactly, are state issues.

In the article I will show examples from managing state in React, but the advice I want to share is more broad and universal.

Where is the lion

Below code with some state definition by useState hook.

const [animals, setAnimals] = useState([]);
const [lionExists, setLionExists] = useState(false);

// some other part of the code... far far away 🌴
setAnimals(newAnimals);
const lionExists = newAnimals
.some(animal => animal.type === 'lion');
setLionExists(lionExists);
Enter fullscreen mode Exit fullscreen mode

What we can see here is clear relation between animals and lionExists. Even more, the latter is calculated from the former, and in the way that nothing more matters. It really means whenever we change animals, we need to recalculate if lion exists again, and if we will not do that, welcome state issues. And what issues exactly? If we change animals and forget about lionExists then the latter does not represent actual state, if we change lionExists without animals, again we have two sources of truth.

The lion exists in one dimension

My advice for such situation is - if your state can be recalculated from another, you don't need it. Below the code which can fully replace the previous one.

const [animals, setAnimals] = useState([]);
const lionExists = (animals) => {
  return animals.some(animal => animal.type === 'lion');
};

// in a place where we need information about lion
if (lionExists(animals)) {
  // some code
}
Enter fullscreen mode Exit fullscreen mode

We have two benefits here:
✅ We've reduced state
✅ We've delayed computation by introducing function

But if this information is always needed? That is a good question, if so, we don't need to delay the computation, but we just can calculate that right away.

const [animals, setAnimals] = useState([]);
const lionExists = 
  animals.some(animal => animal.type === 'lion');
Enter fullscreen mode Exit fullscreen mode

And now we have it, always, but as calculated value, and not state variable. It is always recalculated when animals change, but it will be also recalculated when any other state in this component change, so we loose second benefit - delayed computation. But as always it depends from the need.

What about issues here, do we have still some issues from first solution? Not at all. Because we have one state, there is one source of truth, second information is always up to date. Believe me, less state, better for us.

Error, success or both? 🤷‍♂️

const [errorMsg, setErrorMsg] = null;
const [hasError, setHasError] = false;
const [isSuccess, setIsSuccess] = false;
// other part of the code
try {
  setSuccess(true);
}
catch (e) {
  setErrorMsg('Something went wrong');
  setHasError(true);
}
Enter fullscreen mode Exit fullscreen mode

This one creates a lot of craziness. First of all, as error and success are separated, we can have error and success in the one time, also we can have success and have errorMsg set. In other words our state model represents states in which our application should never be. Amount of possible states is 2^3, so 8 (if we take into consideration only that errorMsg is set or not). Does our application have eight states? No, our application has three - idle state (normal, start state or whatever we will name it), error and success, so how come we did model our app as state machine with eight states? That is clearly not the application we work on, but something few times more complicated.

The pitfall of bad glue

In order to achieve consistent state we need to make changes together. So when we have error, 3 variables need to change:

  setErrorMsg('Something went wrong');
  setHasError(true);
  setSuccess(false);
Enter fullscreen mode Exit fullscreen mode

and when success also:

  setErrorMsg(null);
  setHasError(false);
  setSuccess(true);
Enter fullscreen mode Exit fullscreen mode

Quite a burden to always drag such baggage with us, and remember how these three state variables relates to each other.

Now let's imagine few issues created by such state model:
⛔ We can show error message when there is success state of the app.
⛔ We can have error, but empty box with error message
⛔ We can have both success and error states visible in UI

One state to rule them all 💍

I said our app has three states. Let's then model it like that.

const [status, setStatus] = useState(['idle']);
// other part of the code
try {
  // some action
  setStatus(['success']);
}
catch (e) {
  setStatus(['error', 'Something went wrong']);
}
Enter fullscreen mode Exit fullscreen mode

Now we can also make functions which will clearly give our status a meaning:

const isError = ([statusCode]) => statusCode === 'error';
const isSuccess = ([statusCode]) => statusCode === 'success';
const errorMsg = (status) => {
  if (!isError(status)) {
    throw new Error('Only error status has error message');
  }
  const [_, msg] = status;
  return msg;
}
Enter fullscreen mode Exit fullscreen mode

What benefit this solution has:
✅ We've reduced state variables
✅ We removed conflicting states
✅ We removed not possible states

Our application uses single state to model application status, so there is no way to have both success and error in one time, or have error message with success 👍. Also thanks to state consolidation, we don't need to remember what to change, and what variable is variable relation. We just change one place.

Few words about implementation. I have used tuple, because tuples are ok, but we could be using key-value map like {statusCode:'error', msg: 'Something went wrong'}, that also would be fine. I also made exception in errorMsg as I believe such wrong usage should fail fast and inform developer right away that only error can have an error message.

Add some explicit types

TypeScript can help with more explicit state modelling. Let's see our last example in types.

type Status = ['idle'] | ['success'] | ['error', string ];
const [status, setStatus] = useState<Status>(['idle']);
Enter fullscreen mode Exit fullscreen mode

Above TS typying will allow for no typos, and always when we would like to get error message, TypeScript will force us to be sure it is error status, as only this one has message.

Summary

What I can say more. Putting attention at state modelling is crucially important. Every additional state variable multiplicates possible states of the app, reducing state reduce the complexity.

If something can be calculated from another it should not be state variable, if things change together, consolidate them. Remember the simplest to manage are things which does not change, so constants, next in the line are calculations, so pure functions which for given argument always produce the same value, and the last is state. State is most complicated because it changes with time.

Discussion (15)

pic
Editor guide
Collapse
artydev profile image
artydev • Edited

Nice thank tout
Everytime, I see post about State
I suggest to give an eye to Meiosis pattern

Collapse
macsikora profile image
Maciej Sikora Author

Can you elaborate? I totally don't know what you mean 😉

Collapse
olivierjm profile image
Olivier JM Maniraho • Edited

I think it is this one meiosis.js.org/
I haven't read much about it though.

Thread Thread
artydev profile image
artydev

Read it, you won't regret it 🙂

Thread Thread
merri profile image
Vesa Piittinen • Edited

Not a fan. I've seen people convert to stream based centralized state management and it hasn't worked well from performance perspective. Might be that they've done something wrong, but haven't seen them being able to fix the issue and it's been 1½ years since the change. They did the change because it resulted to "shorter and neater code".

Thread Thread
macsikora profile image
Maciej Sikora Author

Yes, in my experience we always should start from less expressive tool and only if it doesn't work for us search for more sophisticated solutions.

Collapse
artydev profile image
artydev

Excuse-me,
Here is a link to this concept
Meiosis
You can apply it to any front-end framework you want

Thread Thread
macsikora profile image
Maciej Sikora Author

Thanks. Took a look, looks very interesting.

Thread Thread
artydev profile image
artydev

😊 yes it is
Once you try it, you will adopt it

Collapse
artydev profile image
artydev

I have posted several articles on it on devto,
here is one or them reactmeio
Regards

Collapse
meereenee profile image
Enes Kahrovic

Thank you for such an excellent series.
Just one small point. React can help us with delaying the evaluation:

const lionExists = useMemo(() =>
animals.some(animal => animal.type === 'lion'), [animals]);

So, it will be recalculated only when animals is changed.

Collapse
macsikora profile image
Maciej Sikora Author

Yes, that is very good comment. Thank you Enes.

Collapse
remshams profile image
Mathias Remshardt

In regard to the topic of calculated/derived state properties, I always ask myself the two questions:

  • Does it change together
  • Is it not based on coincidence

In case the answer is yes to both questions then one property can be derived from the other (the base one).

This has served me well so far ;)

Collapse
higharc91 profile image
Jarred Beverly

Thanks very informative!

Collapse
mcsee profile image
Maxi Contieri

amazing article!

state modelling is TOO important