Learning from React - part 4
Learning from React series:
- Part 1 - why examining React is useful even if you won't end up using it
- Part 2 - what Facebook wanted to do with React and how to get a grasp on it
- Part 3 - what is Reactive Programming all about?
- Part 4 (this one) - is React functional programming?
- Part 5 - Typescript, for better and for worse
- Part 6 - Single Page Applications are not where they wanted to be
React was designed just when classes and modules were making their way into Javascript, so it made sense to use them. Developers that are not coming from the Javascript or dynamic languages world are used to the type safety and hierarchical structure that classes provide. And it also made sense from the standpoint of the product. If you want to encapsulate state, logic and presentation why not used existing functioning models like classes, components and so on.
However, at the same time ideas like functions being first class citizens of programming languages and functional programming were making a comeback, mostly because of big data. That meant that lambdas (arrow functions) were popping up everywhere. If you are a C# developer, you already are familiar with them. Something like Func<int,int> func = (int x)=> x*2;
represents a lambda function, which is the same as something written like private int f2(int x) { return x*2; }
, yet lambda functions can be declared inside code blocks, can be implicitly cast to Expressions and manipulated and they are brilliant as method parameters. Check out the lambda version in C# compared to the function version in VB:
// C#
var items = allItems.Where(i=>!i.deleted);
// C# function body
var items = allItems.Where(i=>{
return !i.deleted
});
// VB
Dim items = allItems.Where(Function(i) Not i.deleted)
// VB function body
Dim items = allItems.Where(Function(i)
Return Not i.deleted
End Function)
Similarly, Javascript had only function syntax, even if functions were designed to be first class citizens of the language since its inception. Enter arrow functions in Javascript:
// before
var self = this;
var items = allItems.filter(function(i) {
return self.validate(i);
});
// after
var items = allItems.filter(i=>this.validate(i));
Note how arrow functions don't have an internal 'this' so you don't need to bind functions or create self variables.
So at this point, React changed and instead of classes, they implemented "functional syntax" in React Hooks. Behind the scenes a component is still generated as a class which React uses and the old syntax is still valid. For example at this time there is no way to create an error boundary component using functional syntax. The result is a very nice simplification of the code:
// React classic (pardon the pun)
export class ShowCount extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
this.setState({
count: this.props.count
})
}
render() {
return (
<div>
<h1> Count : {this.state.count} </h1>
</div>
);
}
}
// React Hooks
export function ShowCount(props) {
const [count, setCount] = useState();
useEffect(() => {
setCount(props.count);
}, [props.count]);
return (
<div>
<h1> Count : {count} </h1>
</div>
);
}
// courtesy of https://blog.bitsrc.io/6-reasons-to-use-react-hooks-instead-of-classes-7e3ee745fe04
But this does not just provide a better syntax, it also changes the way development is done. Inheritance is basically eliminated in favor of composition and people are starting to use the word "functional" in sentences uttered in the real world. And while the overall design of React to use unidirectional binding and immutable variables was there since inception, I do feel like this is just one more step towards a functional programming approach and the reason for so many functional purists popping up lately.
What is functional programming, though? Wikipedia defines it as "a declarative programming paradigm in which function definitions are trees of expressions that map values to other values, rather than a sequence of imperative statements which update the running state of the program." Sound familiar?
I will have you know that I have friends that have rebelled and gone to the other side, making applications (including UI) with F# and refusing to submit to the Galactic Imperative. After playing with React I can say that I understand why this approach has appeal. One declares what they need, ignore flow and constrain their efforts inside components that are more or less independent. A program looks and feels like a big function that uses other functions and to which you just provide inputs and out comes UI ready to use. If the same input is provided, the same output results. You can test it to perfection, you can infer what happens with an entire tree of such functions and make optimizations in the transpiler without changing the code. You can even use a diff algorithm on the output tree and just update what changed in the UI.
But it is time to call bullshit. We've used functions that receive pure data on one side and output user interface on the other side since forever. They are called views. One could even argue that an API is a data provider and the application is the function that uses the data to output UI. You don't ignore flow, you move it up! You will still have to model the interactions between every piece of data you have and all the events that come in. One might even say the unforgivable and assert that React is just another Model-View thingie with the extra constraint that it will forcibly re-render a component when its input state changes.
That is my main takeaway from React: the idea that forcing re-rendering of components forces the developer to move the state up, closer to where it should be. No one can store stuff in browser variables, in element attributes and data, because all of it will be lost on next render. That is good news, but also very bad news. Let me get you through an example:
We have data that we need shown in a grid. Every row has an expand/collapse button that will show another grid under it, with details related to that row. The React way of doing things would take us through these steps:
- create a component that represents the grid and receives an array as input
- it will contain code that maps the array to a list of row components which receive each row as the input
- the row component will render a button that will dispatch an expand event for the row when clicked
- on click the row expanded state will be changed and the data for the row detail grid retrieved
It sounds great, right? OK, where do you store the state of row expansion? How do we push it to the row component? Let's use a map/dictionary of row id and boolean, why don't we? Does that mean that when you expand/collapse a row only the boolean changes or the entire structure? What will get re-rendered? The row component in question or all the row components?
What happens when we go to the next page in the grid and then go back? Should we return to the same row expansion states? Where should the scrollbar in the grid be? Should we keep that in the state as well and how do we push it to the grid component? Do row details grids have scroll? Doesn't the size of each component affect the scroll size, so how do we store the scroll position? What is the user resizes the browser or zooms in or out?
What happens when we resize a grid column? Doesn't that mean that all row components need to be re-rendered? If yes, why? If no, why? What if you resize the column of a detail grid? Should all detail grids have the same resizing applied? How do you control which does what?
Many grids I've seen are trying to store the expansion, the details, everything in the object sent as a parameter to the row. This seems reasonable until you realize that adding anything to the object changes it, so it should trigger a re-render. And then there is Typescript, which expects an object to keep to its type or else you need to do strange casts from something you know to "unknown", something that could be anything. That's another story, though.
Suddenly, the encapsulation of components doesn't sound so great anymore. You have to keep count of everything, everywhere, and this data cannot be stored inside the component, but outside. Oh, yes, the component does take care of its own state, but you lose it when you change the input data. In fact, you don't have encapsulation in components, but in pairs of data (what React traditionally calls props) and component. And the props must change otherwise you have a useless component, therefore the data is not really immutable and the façade of functional programming collapses.
There are ways of controlling when a component should update, but this is not a React tutorial, only a lessons learned blog post. Every complexity of interaction that you have ever had in a previous programming model is still there, only pushed up, where one can only hope it is completely decoupled from UI, to which you add every quirk and complexity coming from React itself. And did we really decouple UI or did we break it into pieces, moving the simplest and less relevant one out and keeping the messy and complex one that gave us headaches in the first place in? It feels to me like React is actually abstracting the browser from you, rather than decoupling it and letting the developer keep control of it.
After just a month working in this field I cannot tell you that I understood everything and have all the answers, but my impression as of now is that React brings very interesting ideas to the table, yet there is still a lot of work to be done to refine them and maybe turn them into something else.
Next time I will write about Typescript and how it helps (and hinders) React and maybe even Angular development. See you there!
Comments
Be the first to post a comment