React and Redux are a joke right?

2017-08-19

Note: follow up here

This is probably an ill thought through rant but no one reads my blog anyway. I'd tweet where at least a few people read but 140 characters is not enough.

I've been using react for some personal projects and it doesn't completely suck. I like its component nature.

That said, eventually you want to start displaying data and then you run into this issue which is state in react.

Each component in react can have props (data passed down to the component from a parent component) and state (data that is managed by the component itself). React keeps its own retained mode "Virtual DOM" (an internal tree representation of all the HTML elements it can/will generate). When you update state you have to do it in this special way because React wants to know what you changed. This way it can compare the old state to the new state, if nothing changed then it knows it doesn't have to re−render (generate or modify the actual browser elements). Sounds like a win right?

In the context of the browser that's probably a win but let's take a step back from the browser.

Imagine you have an ordered array of people

const orderedListOfPeople = [
  { name: "Andrea",  location: "Brussels", },
  { name: "Fiona",   location: "Antwerp", },
  { name: "Lorenzo", location: "Berlin", },
  { name: "Gregg",   location: "Tokyo", },
  { name: "Rumi",    location: "London", },
  { name: "Tami",    location: "Los Angeles", },
];

Your component either has that as state or it's passed down from the state of some parent as props.

Let's say you're receiving your list of people async (over the net) so you have code to insert a person

function insertPerson(person) {
   const ndxToInsertAt = getIndexToInsertAt(orderedListOfPeople);
   orderedListOfPeople.splice(ndxToInsertAt, 0, person);
}

In the react world though you can't *mutate* your data. You have to make copies

function insertPerson(component, person) {
   const state = component.state;
   const orderedListOfPeople = state.orderedListOfPeople;
   const ndxToInsertAt = getIndexToInsertAt(orderedListOfPeople);

   // must make a new array, can't mutate
   const newOrderedListOfPeople = [
      orderedListOfPeople.slice(0, ndxToInsertAt),  // people before
      person,
      orderedListOfPeople.slice(ndxToInsert),       // people after
   ];

   component.setState({
     orderedListOfPeople: newOrderedListOfPeople,
   });
}

Look at all that extra code just so react and check what changed and what didn't

Let say your request receives an array of people.

getMorePeopleAsync(people => people.forEach(insertPerson));

If you get 3 people react will end up re−rendering 3 times because of the code above. Each call to `setState` triggers a re−render. You have to ADD MORE CODE to work around that issue.

And the hole keeps getting deeper.

Let's say you just want to change the location of a user. Normal code

orderedListOfPeople[indexOfPerson].location = newLocation;

But again react needs to know what you changed, it wants a you to make a new array so

// make a copy of the people array
const newOrderedListOfPeople = state.orderedListOfPeople.slice();

// must copy the person, can't mutate
const newPerson = Object.assign({}, newOrderedListOfPeople[indexOfPerson]); 
newPerson.location = newLocation;
newOrderedListOfPeople[indexOfPerson] = newPerson; 

component.setState({
  orderedListOfPeople: newOrderedListOfPeople,
});

Ok, that's a pain so they came up with immutability-helpers so you can do this

const mutation = {};
mutation[indexOfPerson] = { location: {$set: newLocation} };
component.setState({
  orderedListOfPeople: update(state.orderedListOfPeople, mutation),
}); 

MORE CODE!!

Now imagine you have a tree structure. You end up having to write a mutation description generation function. Given a certain child node in your tree you need to be able to generate a description of how to mutate it. For example if you had this tree

{
  name: "root",
  children: [
    { ... },
    { 
       name: "middle",
       children: [
         { ... },
         {
           name: "leaf",
           children: [],
         },
       ],
    },
  ],
},

In normal code if I have a reference to "leaf" and I want to change its name it's just

leaf.name = newName

In react land using immutable−helpers, first I'd have to add parent references to all the nodes

// no way to reference the parents with static declaration so
function makeNode(name, parent) {
  const node = {
    name: name,
    parent: parent,
    children: [],
  };
  parent.children.push(node);
  return node;
}

const root = makeNode("root", null);
const middle = makeNode("middle", root);
const leaf = makeNode("leaf", middle);

Now, given a reference to leaf I'd have to do something like

function generateMutation(node, innerMutation) {
   if (node.parent) {
     const ndx = node.parent.indexOf(node);
     const mutation = {};
     mutation[ndx] = innerMutation;
     return generateMutation(node.parent, mutation);
   } 
   return innerMutation;
}

const mutation = generateMutation(leaf, {name: newName});
component.setState({
  orderedListOfPeople: update(state.orderedListOfPeople, mutation);
});

SO MUCH CODE!!! All just to set one field of a node in a tree.

Remember what I wrote above that that if you want to modify 3 things, 3 calls to setState will end up re−rendering 3 times. Well imagine the contortions you need to go through to merge the mutation above so it handles 3 arbitrary updates at once.

So then you go looking for other solutions. A popular one is called redux. Effectively instead of directly manipulating data you WRITE LOTS OF CODE to indirectly manipulate data. The set location example above you'd first write a function that made a copy of the person and set the new location. You'd call that an action. You can think of actions like the action in transACTION. You're basically building an transaction system for indirectly manipulating your data.

Let's go back to just setting the location. Redux would want something like this. First they want you to make function to generate *actions*.

function setLocationAction(indexOfPerson, newLocation) {
  return {
    type: SET_LOCATION,
    indexOfPerson: indexOfPerson,
    newLocation: newLocation, 
  };
}

All the function above does is make on object used to describe the thing you want to happen. The type field will be used later to execute different user supplied code based on the type. So we write that code

function setLocationReducer(people, action) {
  // copy the person since we can't mutate the existing person
  const newPerson = Object.assign({}, people[action.indexOfPerson]);  
  newPerson.location = action.newLocation;
  // copy the persons list as no mutation allowed
  const newPeople = [
     people.slice(0, action.indexOfPerson),  // everything before person
     newPerson,                              // modified person
     people.slice(action.indexOfPerson + 1); // everything after person
  ];
  return newPeople;
}

Now you register setLocationReducer with redux and then when you want to set the location you'd do something like

dispatch(setLocationAction(indexOfPerson, newLocation));

That generates an *action* object, then using the *type* field ends up calling setLocationReducer all to indirectly set the location of a person

NOTE: I'm new to redux so the example above might not be perfect but that's irrelevant to my point. Please keep reading.

So what about trees of data with redux? The docs tell you try not have nested data. Instead put things in flat arrays and use indices to reference things in other flat arrays.

Read around the net about how everyone seems to think redux is the bees knees (as in they love it).

But take a step back. All of this extra code is because of React. React wants to know which data changed so it doesn't re−render too much. People say React makes the UI simpler but it makes dealing with our data MUCH HARDER! Why is the UI library dictating how we store and manipulate our data!!!

Now, there may be good reasons. I get we're working in the browser, the browser uses the DOM. The DOM is complicated. Each node is very heavy, hundreds of properties. Each node has hundreds of CSS styles directly or indirectly. The browser handles all of this and multiple non−ASCII languages and fancy styles and CSS animation and all kinds of other stuff.

BUT, ... Go try using something like Dear ImGUI. You store your data however you want. You manipulate your data however you want. You then have your UI use that data in place. No extra code just manipulate your data just because the UI framework needs it. And it's running at 60fps with very complicated UIs. No jank! Want to see it running live here's the sample code running in the browser.

Now I get that ImGUI does much less than the browser. See 2 paragraphs up. But that's not the point. The point is to look at how much of a pain it is to use React (and probably the DOM in general). To notice all the hoops it's asking you to jump through. To notice that maybe your UI code got simpler than direct DOM manipulation but your data management code got 10x harder. It feels like we need to take a step back and re−evaluate how we got in this mess. There has got to be a better way!. We shouldn't need immutability helpers and/or redux and actions and reducers. Those are all solution for a problem we shouldn't have in the first place. We should just be able to have our data and manipulate it directly and we should be able to have a practically stateless UI system that can run at 60fps no jank.

Maybe someone needs to start the browser over. window.ImGUI. The no DOM use path. I'm sure that idea has issues. That's not the point again. The point is to see how much extra work we're being told to do, how many hoops we're being made to jump through, and then consider if there are better solutions. I don't know what those solutions are and I'm really not trying to dis React and Redux and the whole environment. Rather I just feel like setState, immutability helpers, redux, and all the other solutions are going down the wrong path solving a problem that shouldn't be there in the first place. If we solved the original issue (maybe that the DOM is slow and possibly the wrong solution) then the need to do all this extra work would disappear.

Comments
CSS Grid - Fail?
Rethinking UI APIs