React.js is a hammer

home

If you've written event code you may have seen this problem: to switch from X to Y, you first need to clean up X. And it's so easy to forget something, or clean up something that should have stayed. With more things to switch between the problems balloon. Sometimes I wish I could erase everything and remake it from scratch. That's safe and easy, but totally impractical. It's also how React.js works.

React.js doesn't mess around with tweaks to premade html. Code makes everything, controlled by your "state" variables. When a state variable changes, React automatically reruns the "make everything" code. After you write all that, events are easy -- they just change the state and let React do the rest.

Well, maybe not easy. The state variables aren't global. You can only change your own, or ones a parent passes you a function for. If button B needs to make a change that will affect paragaph P, it needs to be passed up to a common parent, then down to P. Yeesh.

All-in-all, React isn't something you just add, like jquery. It's a completely different philosophy and a total redo of your page. Easy things are hard, but (I assume) hard things -- getting clean changes between multiple options -- are easy.

Basic layout

React creates and maintains a shadow version of the real HTML nodes. Those nodes come in 2 flavors: with and without state (variables). Why? Why not 1 way with an option to add state? Because making nodes with state is a huge pain. So that's nice. A picture:

  React nodes          HTML nodes (auto-created)
  ----- -----          ---- -----
  root (state)         root (marked with an ID known to React)
    table1 (state)       table1
      tr          ==>      tr
      tr                   tr
    table2               table2
      tr                   tr

 statechange <=== react function <=== HTML EVENT
     \
      ==> update react nodes ==> redo HTML

Coding a page

The things making up the React tree nodes are elements. The functions making elements are confusingly called components. If you forget which is which, component-functions end with a call to React.createElement.

A simple "component" using the stateless-style. It makes a tabel entry with any text and color:

function coloredTD(theTxt, theColor) {
  var t = theText;
  if(t=="") t=" ";
  return React.createElement("td",{"color":theColor}, t);
}

// eventual results of coloredTD("hat","blue"):
// <p color="blue">hat</p>

createElement's first input is the type of thing to make. React has some idea what the tags mean. The second input are the values that go inside the tag, written as a javascript Map. The rest of the inputs are the children or the text. Or a mix of both, for example:

  const boldPart=React.createElement("b", {}, "like");
  return React.createElement("td", {}, "I ", boldPart, " fish");

  // <td>I <b>like</b> fish<td>

A component with complex children would make them first. This uses coloredTD to create elements in row:

function greenRow(TextList) {
  var theTDs = [] // a list of completed elements
  for(int i=0; i<TextList.length; i++) {
    theTDs[i]=coloredTD(TestList[i], "green");
    // NOTE: coloredTD is our previous component
  }
  return React.createElement("tr",{}, ...theTDs);
}

//sample run:
const row1=greenRow(["cat", "rat", "balloon"]);
// <tr><td color="green">cat</td> ...

... (three dots) is the new javascript notation to unroll a list into individual inputs.

Note that these don't actually make HTML. They make React elements, which go into the React trre somehow, which eventually produce the HTML.

The final step is a single call to ReactDOM.render. It takes a completed element -- probably a DIV or something else holding lots of stuff == andwhere to put it. We're assuming the page has an ID we can find, marking where React should go:

// get actual place in existing page:
const loc=document.getElementById("rowGoesHere");

// create a row element with these words:
const row=greenRow(["cats","rats","cheese"]);

// display it as HTML:
ReactDOM.render(row, loc);

Alternate JSX syntax

React adds a clever syntactic shortcut for createElement. If you write naked HTML-style it will convert into a pile of createElement's:

function makePwithBold(t1, boldText, t2) {
  return <p>Cow facts: {t1} <b>{boldText}</b> {t2}</p>
}

The curly-braces are a special syntax for including variables inside JSX. Of course, javascript doesn't understand it. It translates to:

function makePwithBold(t1, boldText, t2) {
  const boldPart=React.createElement("b",{},boldText);
  return React.createElement("p",{}, "Cow facts:",t1,boldPart,t2);
}

As you might guess, JSX is the preferred way to write React code.

JSX is very slick and at the same time somewhat pointless. It allows us to write normal HTML, which converts to code, which makes the original HTML. But if we weren't using React we would need those extra 2 steps. We'd write the same HTML, as regular HTML. So, really, JSX makes React's "everything created in code" rule somewhat less awkward. Even so, microsoft's Typescript liked it so much they stole it.

JSX can be mixed in with code. This puts "like" in a bold element, saving us a line:

React.createElement("p",{}, "I ", <b>like</b>, " milk"); 

There's a trick which is sort-of the opposite. If JSX uses a component name as a tag, it runs that component with the JSX as input. This will attempt to run a "component" function named MakeRow, adding "duck" as a child:

<MakeRow cats="4">duck</MakeRow>

Using state

To have a React page that does stuff, some of the elements need to have "state" variables. Making an element with state is a mess. Here's a simple state-component making a TD element which can be selected on a click (it's very crude -- it can't be unselected. Part I:

class niceTD extends React.Component {
  constructor(props) {
    super(props);  // this is required

    this.contents=props.theText; // input is: {"theText":someText}
    this.state={"selected":"no"}; // <--state!

    // standard javascript nonsense to make functions know "this":
    this.handleSelectToggle=this.handleSelectToggle.bind(this);
  }

You have to declare it as a class. That's odd, since javascript doesn't have classes and class is just a shortcut for making functions with properties, but it's the rule. React components with saved variables need to be created this longer way.

Notice how we manually create a variable named state. That's the magic word React redraws key off of. Later we'll be more formal and use React function setState to change it.

Next we write a familiar-looking function to turn it into an element. It's named render(), which React knows to run automatically, as needed:

  render() {
    var prpty={"onMouseOver":this.handleSelect};
    if(this.state.selected=="yes") prpty.color="red";

    const item=React.createElement("td", prpty, this.contents);
    return item;
  }

It's a bit of a pain since we can't directly pass parameters to render(). The constructor gets them, saves them, and render reads them back. Ug. But if(this.state.selected=="yes") is the regular way we use state variables. Nothing fancy -- we just look at them and decide what to do.

In that bit of code, onClick was set to call our handleSelect function. We want a click to change our selected to "yes", but javascript can't do that directly. Thus we create and pass it a function. That's going to be a very wearysome part of writing React code. The last part of the class:

  handleSelect() {
    this.setState({"selected":"yes"});
    // setState also triggers a redraw
  }
}  // end of niceTD class

setState is a magic function we got from React.Component. If will change or add to the state, and knows to do a redraw (but React is smart enough to wait until after every state change).

And that's it: put the creation code in render, use the contructor to set-up state and remember inputs, and add callback functions to change state.

We run it with a regular createElement, except the inputs need to be in map form. This runs the constructor, and then render:

  const e1=React.createElement(niceTD, {"theText":"cow"});

We can use e1 as someone's child, the same as any other element.

One funny thing is going on here. React looks as if it's completely remaking elements each time, but it's not. That would destroy the state we worked so hard to save. React only runs the constructor the first time. When it sees the element already exists, it only runs render().

How React passes functions

Recall that javascript events takes strings. onMouseOver="eat(this,4); drink()" is fine. React is different. You can only use a function, with no inputs. It has to be something like {"onClick":drink}. Things like props={"onClick":setEggs(3)} are no good -- it tries to run setEggs(3) immediately.

One solution is to create functions doing exactly what you need, like writing setEggsTo3. You can, but it's a huge pain. The common hack is using a lambda function: ()=>setEggs(3) is a function. It has no inputs and runs setEggs(3) when called. Clever, right? In general, if you want "f(a,b)" use ()=>f(a,b).

Wait? Aren't js classes and functions the same?

As we know, javascript "constructor" functions and classes are the same. Things made with class immediately turn themselves into functions. They're different in React because React went out of their way to make it so.

Calling a React function-component is expected to return an element. Calling a React class-component is expected to return an object which can eventually create an element. There's no reason for the difference -- function style could return an object with a render function. React made their own rule "if you see a class tag, treat it like X instead of Y.

Once they created this difference, the other rules are obvious. Function-components have inputs used to directly to create the element, since they directly create the element. Class-style element creation is deliberately 1 step removed, not under your control. So of course we can't directly send inputs to class-element creation.

Communicating

React tightly constrains who you're allowed to talk to. There's nothing like getElementById. The main communication is to your parent, through a function created for that purpose and passed down to you. It's a huge hassle.

Say we want an item with 4 child buttons to select 0-3 eggs. First we'll make the parent class with the egg count (in state "eggs"):

class eggHolder extends React.Component {
  constructor(props) {
    ...
    this.state={"eggs":0}; // <-- eggs start at 0
  }

Our 4 button-children can't just reach up and change eggs. They need to be given an egg-setter function. It's simple, but required:

  setEggs(n) { this.setState({"eggs":n}); }

As we create the buttons we'll pass setEggs (using the ()=> trick):

  render() {
    // create each button with onClick using setEggs:
    const b0=createElement("button", {"onClick":()=>setEggs(0)}, "0 eggs");
    const b1=createElement("button", {"onClick":()=>setEggs(1)}, "1 egg");
    ...
    const showEggs=React.createElement("p",{}, "There are "+this.state.eggs+" eggs");
    return React.createElement("div",{}, showEggs, b0, b1, b2, b3);
  }
}

This is the general pattern: an element with state will make setter functions for it's state, which is gives to children who need it. It seems funny having regular javascript onClick's call react element functions, but React is javascript and our "React setters" are just javascript functions. So it's fine.

What if we aren't directly making our children? What if we're using another React component function? No problem -- we pass in the setter. Here makeNumButton expects to be given a change eggs function:

makeNumButton(itemName, amount, changeVal) {
  prpty={"onClick":()=>changeVal(amount)};
  return React.createElement("b", prpty, "I'll have "+amount+" "+itemName");
}

Our parent simply supplies setEggs:

  // in the parent's render:
  const b0= React.createElement(makeNumButton("eggs", 0, setEggs), {});
  const b1= React.createElement(makeNumButton("eggs", 1, setEggs), {});
  ...
  return React.createElement("div",{},showEggs,b0,b1,b2,b3)

Maybe I'm crazy, but this is awkward.

For fun, one more example. Clicking a row selects it (turning it green) and unselects the old row. makeTR is at the bottom. It makes one row from a list of strings:

function makeTR(TDtexts, rowNum, isSelected, callback) {
  var TDs=[];  // each created element in the row

  var TDprops = {};  // elements in the row share these
  if(isSelected) TDprops.style={"color":"green"};

  for(var i=0; i<TDtexts.length; i++) {
    TDs[i]=React.createElement("td", TDprops, TDtexts[i]);
  }

  const prpty={"id":"row"+rowNum, "onClick":()=>callback(rowNum)};
  if(isSelected) prpty.style={"outline":"2px solid blue"};

  return React.createElement("tr", prpty, ...TDs);
}

The top level is a table with 1 state integer, "selected":

class makeTable extends React.Component {
  constructor(prpy) {
    super(prpy)
    this.state={"selected":-1}; // nothing starts seleected

    // standard javascript trick:
    this.getNewSelection=this.getNewSelection.bind(this);
  }

  // the required setter for "selected":
  getNewSelection(newSelNum) {
    this.setState({"selected":newSelNum});
  }

  render() {
    // premake each row as an element:
    var rows=[];
    const Tvals=[["a","b","c"],["cat","dog","ant"],["X","Y","Z"]];
    for(var i=0;i<Tvals.length;i++)
      rows[i]=makeTR(Tvals[i], i, this.state.selected==i, this.getNewSelection);

    const tbl=React.createElement("tbody", {}, ...rows);
    return React.createElement("table",{"style":{"border":"2px solid orange"}}, tbl);
  }
}

To finish up we create and install the whole thing:

const wholeThingEx1=React.createElement(makeTable, {}); 
const ex1Loc=document.getElementById("ex1");

ReactDOM.render(wholeThingEx1, ex1Loc);

Changing state in a child

You can easily change your function-style stateless children by calling them with new parameters. But how do you change your class-style children? If your child has state "eggs", how can you change it? You only make it once, so you can't remake it again with a different amount of eggs. And you can't use the saved refernce to the element when you made it (see far below). You have to use a new special react rule.

When you make the element, add ref:"someName" to the properties. That tells React to stores a reference. Ex:

  const e1 = React.createElement(compClassA,{...props, ref:"e1ref"});
  // alternately: props.ref="e1ref"

this.refs.e1ref is the new reference. refs is a special spot React creates to hold these things. If you had another using ref:"e2", it would be in this.refs.e2.

You can now use it as a normal reference. This reaches down to e1 and changes its animal:

  this.refs.e1ref.setState({"animal":"hen"});

Here's working code showing the ref trick in use. Clicking the paragraph reaches down to change the wrds state in the child bold element:

class refTestBold extends React.Component {
  constructor(props) {
    super(props);
    this.state={"wrds":"starting bold"};
  }

  render() {
    return React.createElement("b",{},this.state.wrds);
  }
}

The parent class creates a paragraph with the bold refTextB inside. Its click event reaches into the bold child using the ref:

class refTestRoot extends React.Component {
  constructor(props) {
    super(props); // required

    // standard javascript to fix the function:
    this.clickCallback=this.clickCallback.bind(this);
  }

  clickCallback() {
    // refs and refs.myBref were auto-created in render():
    this.refs.myBref.setState({"wrds":"words after click"});
    // triggers a redraw (even though it was someone else's state)
  }

  render() {
    // create child, save reference in refs.myBref:
    const myB=React.createElement(refTestBold,{ref:"myBref"});

    // give myself a click event:
    const prps={"onClick":this.clickCallback};

    return React.createElement("p",prps,"main para ", myB);
  }
}

A last note: can't we just use the regular variable? When we use const e1=React.createElement(refTestB,{}); can't we just save e1 and use e1.setState? Nope. It turns out that elements aren't done yet. createElement has an invisible part we can't see. That's what we need a reference to, and only the ref: trick can get it. Whew!

Just use javascript

Since React is just javascript, you can always ignore the way you should do things and call a regular javaascript function. If you want to change the text in describeBox you're suppose to call some function your parent gave you, which blah, blah, blah ... . But we can write straight-up javascript to do it directly:

function setDescribeText(t) {
  var db=document.getElementById("describeBox");
  // describeBox may have been created in React, but it has an ID
  db.innerHTML=t;
}

  // in a component:
  var props={"onClick":()=>setDescribeText("a fur coat")};
  const e=React.createElement("td", props, "coat");

This is total abuse. It will break if you do anything else (the next React redraw will erase it). You could add javascript globals to handle that. At that point you're not really using React, but I wonder if many React-using projects resort to this for the final touches..

 

 

 

Comments. or email adminATtaxesforcatses.com