State management beyond the map
There's a way of building web applications where views are declarative. They say what to render and not how exactly should it be done. Data flow is unidirectional, what means it just receives data but never changes it directly. Instead of that, it sends back commands describing what should be done. Then it's rendered again. I used this stack to create a simple ToDo App.
It looks like that.
Here goes the code.
<?php
require_once __DIR__ . "/common.php";
$viewData = [];
if (!isset($_SESSION['todos'])) {
$_SESSION['todos'] = [];
}
if (isset($_POST['add']) && isset($_POST['newContent'])) {
$_SESSION['todos'][] = [
'done' => false,
'content' => $_POST['newContent']
];
}
if (isset($_POST['mark_done'])) {
foreach (array_keys($_POST['mark_done']) as $id) {
if (isset($_SESSION['todos'][$id])) {
$_SESSION['todos'][$id]['done'] = true;
}
}
}
$e = 'htmlspecialchars';
$viewData = [
'todos' => $_SESSION['todos'],
'newContent' => isset($_POST['newContent']) && !isset($_POST['add'])
? $_POST['newContent']
: ''
];
?>
<form method="POST">
<ul>
<?php foreach ($viewData['todos'] as $id => $todo): ?>
<li>
<?php if ($todo['done']): ?>
<s><?= $e($todo['content']) ?></s>
<?php else: ?>
<?= $e($todo['content']) ?>
<input
type="submit"
name="mark_done[<?= (int)$id ?>]"
value="Mark as done"
/>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<input
type="text"
name="newContent"
value="<?= $e($viewData['newContent']) ?>"
/>
<input type="submit" name="add" value="Add a ToDo" />
<input type="hidden" name="_csrf" value="<?= $e(csrfToken()) ?>" />
</form>
Yup. It's an old-fashioned, server side PHP script.
Don't get me wrong, I fully admit that many things have changed since then. We’ve got wonderful auto-escaping mechanisms which greatly improved security (JSX was born as one of them). Protection against CSRF attacks has become a common thing (at least in mature back-end frameworks). And we have finally moved a vast part of user interfaces to the front-end (JS is not anymore a tool to embed dancing hamsters on a website). It all provides more beautiful and responsive user interfaces.
But there's something that hasn't changed at all. It's the way how we manage changes of behavior. In the above example there's exactly one set of bahavior. There are always the same controls and they always to the same. The form is always there and it always adds a new ToDo. The list is also visible all the time and it just displays all the ToDos.
But what if there are different screens? What if the user may take different paths? What if the app looks list this?
The state of the art in web interfaces is to save some values within the state and then use conditional statements to pick the appropriate behavior. There may be some boolean values stating that the user has performed some actions, some integers (or enums) representing the current step etc. And then we use if statements (or ternary operators, switch statements or table lookup techniques) to decide what to do. The whole architecture may be summed up as spreading conditional statements over a map (or a session, a database, a dumb object - you name it). We may avoid duplication of if statements, but we can't avoid them completely even if we wanted to.
Once I was asked if I could introduce "few little changes" to an app I was maintaining. When I realised what kind of variables would I need to check and how many complicated conditional statements would be necessary, it terrified me! It seemed barbaric!
What's interesting is that the description of those changes wasn't so tangled. It consisted few scenarios like "if the user does this, then she's finally going to land on this screen" etc. It made me wonder why was it so hard to code it, if it was so easy to talk about. Soon I understood that the high level architecture I had was suitable only for simple screens which never change, but it by no means reflected the domain of the problem.
Actually the "non-technical people", as we like to call them, came to me with a much more suitable and elegant architecture. Even though they didn't use that exact word, what they described was one of the fundamental concepts in computer science - a state machine. It was me who didn't see that and instead tried to utilize maps and if statements. Such a shame!
If I would be about to chose one experience, which made me willing to explore practical application of state machines in web development, it would be this one. Some time after that, I published the first version of Rosmaro. I actually used it to create the second application presented in this post. You can find a detailed description of its architecture in the post describing how state machines come to the rescue of complex forms. Oh, and let me share something with you. It made me really happy when I recently stumbled upon a great talk given by David Khourshid - Infinitely Better UIs with Finite Automata. If you're interested in how state machines may improve the architecture of user interfaces, definitively check it out. I bet you won't regret it!