DEV Community

Isaac Hagoel
Isaac Hagoel

Posted on • Edited on

Conceptual Gaps in Declarative Frontend Frameworks - Part 1 - All Props are Created Equal

TLDR: Props cannot express what we (well.. at least I) need them to express

Introduction and Context

This article is meant to be a part of of a short series in which I point out some overlooked trade-offs made by the declarative approach to describing user interfaces.
The declarative approach is the de-facto standard in the industry and was adopted by the major frontend frameworks and even by the built-in vanilla web-components.
There are two main reasons I think this topic is worth exploring:

  1. The advantages of declarative programming in the context of frontend development are well understood and frequently mentioned but the disadvantages are rarely ever acknowledged.
  2. As far as I can tell, these disadvantages hold the community back from writing richer user interfaces and more expressive (readable, maintainable, effective) code.

I've used three web-frameworks (not at the same time :)) to build relatively large UIs: React (please stop calling it a library), Svelte 3 and Aurelia. They are all wonderful in their own ways but share the issue I am going to describe. I've also used vanilla javascript with custom elements, which allow working around this issue if you are willing to accept a whole bag of other issues :).

I have not used Vue, Ember, Angular, Polymer and countless other frameworks in any meaningful capacity. Please do let me know if any framework out there is conceptually different in how it models props.
I am not trying to bash the declarative style or any framework nor am I trying to promote any agenda or silver-bullet solution.

My goal here is to provide some food for thought and ideally learn from the feedback I get back.
I am using React in the examples below because I assume most readers are familiar with it.

Let's Talk Props

With all of that out of the way, let's have a look at how you would express that some UI component needs to be on the screen in a typical declarative manner. It would probably be something like:

<MyComponent prop1={val1} prop2={val2} ... />
Enter fullscreen mode Exit fullscreen mode

What is the contract from the point of view of whoever uses MyComponent? Just give it a bunch of mandatory/ optional props and it will present something that correlates to these props on the screen. To quote the React docs:

Conceptually, components are like JavaScript functions. They accept arbitrary inputs (called β€œprops”) and return React elements describing what should appear on the screen.

Pretty straightforward, right? Not so fast...

Notice that what happens when/if you decide to change any of the props after the initial rendering is not a part of the contract.
Take a second to think about it...
"Conceptually, components are like JavaScript functions" they say, but to which extent are they really conceptually alike?

Is rendering MyComponent with prop1=5 and then changing prop1 to 3 equivalent to rendering MyComponent with prop1=3 in the first place? In other words, is MyComponent a pure function in regards to prop1? Is it a pure function in regards to prop2 (can be a different answer)? Can you tell by looking at this JSX/ template?

Have you ever wondered why writing pure functional components (read: the original ones, without hooks) in React feels so good? Here is your answer, or at least part of it:
The truth is that the only thing this kind of syntax can represent faithfully is a pure function (and even that is arguable).

What if MyComponent is a stateful/ side-effectful entity that exists over time and is not re-created on every prop change?
The syntax above tries to ignore this very real and very common possibility. It assumes purity.

Let's look at how this assumption breaks via a concrete example:

The initial value is passed into the child component as a prop and used as you would expect, to initialize the value :)
There is also a '+' button that allows you to increment the value after it was initialised.
Any subsequent change to the initial value prop (which you can make using the input box) has no effect over the actual value. It has already been initialized and the child component does not use it as part of its rendering logic. To be clear, from the child component's perspective this is the intended behaviour, not a bug.
React gives us no way of distinguishing between this kind of prop (in this case, some kind of initial setup) and the props that are used on every render. The props interface pretends there is no difference. It forces us to provide all of the values every time in a flat list.

Here is the code for this example:

import React, { useState } from "react";
import PropTypes from "prop-types";
import "./styles.css";

export default function App() {
  const [initialValue, setInitialValue] = useState();
  return (
    <div className="App">
      <h2>Configuration prop?</h2>
      <label htmlFor="init">Set initial value:</label>
      <input
        id="init"
        type="text"
        pattern="[0-9]*"
        value={initialValue || ""}
        onChange={e =>
          e.target.validity.valid
            ? setInitialValue(e.target.value)
            : initialValue
        }
      />
      <hr />
      {initialValue !== undefined && (
        <Configurable initialVal={parseInt(initialValue, 10)} />
      )}
    </div>
  );
}

class Configurable extends React.Component {
  constructor(props) {
    super(props);
    this.state = { value: props.initialVal };
  }
  render() {
    const { value } = this.state;
    return (
      <div>
        <h4>Configurable (class) component</h4>
        <span>Value: {value} </span>
        <button
          type="button"
          onClick={() => this.setState({ value: value + 1 })}
        >
          +
        </button>
      </div>
    );
  }
}

Configurable.propTypes = {
  initialVal: PropTypes.number.isRequired
};
Enter fullscreen mode Exit fullscreen mode

This might be a silly example but I encounter these kind of situations quite frequently in the real world. Think about passing-in a baseUrl that is used in componentDidMount (or useEffect/ useLayoutEffect with an empty dependencies array) in order to retrieve some assets; or how about some prop the developer wants to protect from changing after initialization - like session ID?
Are you tempted to tell me to stop complaining and just look at the documentation? If so, we agree that the code itself is not and cannot be expressive enough. What a strange thing...

Hooks make it even worse in this case. Let's see the same example implemented using a functional component instead of a class.

Here is the functional implementation of the Configurable component (App stays the same):

function Configurable({ initialVal }) {
  const [value, setValue] = useState(initialVal);
  return (
    <div>
      <h4>Configurable (functional) component</h4>
      <span>Value: {value} </span>
      <button type="button" onClick={() => setValue(v => v + 1)}>
        +
      </button>
    </div>
  );
}

Configurable.propTypes = {
  initialVal: PropTypes.number.isRequired
};
Enter fullscreen mode Exit fullscreen mode

Take a minute to think about how misleading this is. Even though a new initial value is directly passed-in to useState every time the prop changes, it is completely ignored (expected behaviour, I know, it is not the behaviour I am complaining about but the API design).
In the class implementation at least it was explicit; One look at the render function would make it clear that the initial-value prop is not involved.
Hooks try to pretend that everything can be expressed as rendering logic and in that sense add insult to injury.

Solution?

To be honest, I don't know what a good solution might be. It is tempting to think that separating the flat list of props into several smaller lists could be a step in the right direction. Something like:

<MyComponent initialization={prop1=val1, ...} rendering={prop2=val2, ...} ... />

Enter fullscreen mode Exit fullscreen mode

This might be better than nothing but it doesn't prevent me from changing the value of prop1 on the fly, which will be ignored.

In Imperative-land this issue doesn't exist. The imperative version would looks something like:

const myComponent = new MyComponent({'prop1': val1});
myComponent.attachTo(parentElement);
myComponent.render({'prop2': val2});
Enter fullscreen mode Exit fullscreen mode

For a non-pure component like ours, this is much more expressive and flexible, isn't it (and no I am not suggesting we switch back to JQuery)?

I have to ask: are props the best API we could come with? Do they deserve to be the standard?
Even an otherwise groundbreaking framework like Svelte doesn't seem to question them.
I wonder whether there is a better abstraction than props out there.
One that has semantics that are less detached from the underlying reality.
If you have an idea for one or are familiar with one, please do let me know.
Thanks for reading.

Top comments (6)

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Seems to me like there's a trade-off that's been made by the broader JS community, and that trade-off is causing some of your (understandable) angst with regard to props. The trade-off I'm talking about is in regard to two-way data binding.

With two-way data-binding, you bypass a lot of this confusion because there's no distinction between "initial value" and "current value". There's just one value. Sure, you set it to some "initial" value, but once that value is updated - from anywhere - the initial value is destroyed.

I used to do some Flex development and, once I got my head around it, I really kinda loved it. And when KnockoutJS came out, I thought that's where we were heading in the JS world. But something funny happened on the way to the forum...

"Two-way data-binding" seems to have become a dirty word in the JS community. Although I understand some of their gripes, I definitely don't see two-way data-binding as a bad thing, and I kinda wish people hadn't decided that it's voodoo, but, whatever...

The point is that, when the "next wave" of frameworks came out, they seemed to be hellbent on not implementing two-way data-binding. But they still need to have some way of updating values (state) if they're gonna be of any use. And thus... you get this dichotomy between how something was initially represented and how it's currently represented.

Collapse
 
isaachagoel profile image
Isaac Hagoel

Thanks much for taking the time to read and comment. I agree with your observations. The distinction I am looking for is between props that are relevant for rendering and props that aren't. Props that are allowed to change and props that don't (sometimes it is okay to update the initialValue on the fly but it might require work as loading new assets etc).
The 'props' interface treats all of them as subject to change at any time and doesn't allow you to express anything else. I have seen components in which maybe 10% of the props were subject to change after initialisation (the rest was configuration, static callbacks and such). Trying to understand and debug those can be a pain.
Because I have a lot of experience with languages like Java in which you have proper constructors, methods (beyond just 'render') and so on, I sorely miss this richness of functionality here and feel forced to dumb down my designs πŸ˜”

Collapse
 
frankdspeed profile image
Frank Lemanschik

I love this article and i want to share a framework with you canjs.com has the best props state managment its the only Real MVVM Framework that is really complet.

but as you sayed you avoid frameworks i want also your judgement and ideas for this dev.to/frankdspeed/the-html-compon...

Thanks in advance.

Collapse
 
isaachagoel profile image
Isaac Hagoel

Thanks! I'll have a look when I have some time and get back to you

Collapse
 
isaachagoel profile image
Isaac Hagoel

canjs looks interesting for CRUD use cases!
commented on your post.
cheers

Collapse
 
andyjessop profile image
Andy Jessop

Isn't the problem here that the component is statefull, rather than a problem with the props pattern itself? If the state were managed outside the component then the problem doesn't exist