DEV Community

Oinak
Oinak

Posted on

React first steps

They started using react at work, so I set up myself for the bare minimum tutorial-based experiments (watch your step! I am learning while I type the post).

You can use jsbin or repl-it for this, but I already had yarn installed so I copied the config from repl.it example:

Config (yarn):

{
  "name": "runner",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.9.0",
    "react-dom": "^16.9.0",
    "react-scripts": "2.1.5"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}
Enter fullscreen mode Exit fullscreen mode

With this you can install dependencies with the yarn install command.

Minimal app:

Html:
I only added <div id="app"></div> to a basic and empty HTML5 file because React needs an element to render into.

Saved on public/index.html per yarn convention.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>React 101</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Javascript:

Saved on src/index.js per yarn convention.

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  <h1>hello world</h1>,
  document.getElementById('app')
)
Enter fullscreen mode Exit fullscreen mode

Build this with yarn build

This is needed because I am going to use JSX to write React equivalent of templates. JSX is a language that translates to normal html but allows variable interpolation and some logic embedding.

You can use React without JSX (using createElement or e helpers), or use it without a build step (by compiling with a babel script), but using it is the most popular option, and mostly trivial if you are going to have a build step anyway. I do not recommend it (I don't have enough experience on React to have an informed opinion), just explain why I am using it for this post.

Test it on your browser with yarn start

It will default to serve locally on localhost:3000, but so does Ruby on Rails so if you are using both on your machine, do not try to run them at the same time, or change the configuration on any of the two.

Output:
Helo world output

Components and props

Let's add what React calls a component, i.e. a separate part of the interface with its own markup, logic and state.

// imports omitted from now on for brevity

function Hello(props) {
  return <h1>Hello, {props.name}</h1>;
}

ReactDOM.render(
  <Hello name="Oinak" />,
  document.getElementById('app')
);
Enter fullscreen mode Exit fullscreen mode

Output:

Hello me

A lot happened here:

A function receiving props and returning JSX is a minimal component.
function f(props){ return <span>any jsx</span> }

Curly braces allow interpolation inside JSX;
Hello {props.name} becomes "Hello Oinak"

A tag on capitals is replaced by a component of the same name, and its attributes become props:
<Hello name="Oinak" /> calls Hello({ name: 'Oinak'}) and is replaced by its output: <h1> Hello, Oinak</h1>.

Function components are a shorthand for full fledge ES6-style classes:

// function Hello(props) { return <h1>Hello, {props.name}</h1>;}
class Hello extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
Enter fullscreen mode Exit fullscreen mode

They mean the same, but the function is shorter if you don't have to do anything with the constructor, state, etc...

So let's build an app that actually does something, I am going to go crazy original here and build a To-Do list because it's something no one ever in the history of the internet used to learn a js framework.

So first, I take the code from before and create a component for the input:

class Input extends React.Component {
  render() {
    return (
      <div className="Input">
        <input type="text" />
        <input type="button" value="+" />
      </div>
    );
  }
}

ReactDOM.render(
  <div>
    <h1>TO-DO</h1>
    <Input />
  </div>,
  document.getElementById('app')
);
Enter fullscreen mode Exit fullscreen mode

Now the Input component has a text box and a button with a plus sign on it.

The idea is that you write your list item text on the box and click on the '+' button when you are done.

This code is good enough for the input GUI:

To-Do item input

But it does nothing.

I need two more things, the code to store new items and to display them. Let's start with the latter:

I chose to represent the list as an html ordered list, so each item is simply a list item <li>Like this</li>. With that idea, Item component can be like this.

class Item  extends React.Component {
  render(){
    return <li>{this.props.text}</li>
  }
}
Enter fullscreen mode Exit fullscreen mode

This code assumes that you call it like this: <Item text="Hello"> so that a text attribute gets saved into props by the default React constructor.

Now, I change the main call to ReactDOM.render to use the Item component:

ReactDOM.render(
  <div>
    <h1>TO-DO</h1>
    <Input />
    <ol>
      <Item text="Hello" />
      <Item text="World" />
    </ol>
  </div>,
  document.getElementById('app')
);
Enter fullscreen mode Exit fullscreen mode

Then you get this:
Full GUI

We have a mockup!

For the next steps we need some new concepts:

Event handling

State:
We set initial state in the constructor via this.state = ... but when components render depends on their state, we need to tell_ React that we need a new render, thats what the setState method is for, it updates the state and triggers a new render. There are two versions:

this.setState({ key: value });
Enter fullscreen mode Exit fullscreen mode

and, if current state depends on previous state or props:

this.setState(function(state,props){
  return {
    // something based on previous state or props
  };
})
Enter fullscreen mode Exit fullscreen mode

We need also function binding, to keep event handlers' this bound to the component.

class Item  extends React.Component {
  constructor(props){
    super(props);
    this.state = { done: false };
    this.toggleDone = this.toggleDone.bind(this); // bind this
  }

  toggleDone() {
    // this is the component because of the binding
    this.setState({done: !this.state.done, render: true});
  }

  render() {
    // change style depending on state:
    const elementStyle = (this.state.done ? {textDecoration: 'line-through'} : {});
    return (
      <li style={elementStyle}>
       <input type='checkbox' value={this.state.done} onClick={this.toggleDone} />
       <span> {this.props.text} </span>
      </li>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

With this, we are able to change the state of Item components, and React will automatically change their rendering.

Before click:

List with checkboxes

After click:

Strikethrough element

Inline styles won't make your design pals happy, but we will get to that later.

Handling events outside component

Now we have a problem, the interface for adding elements is in the Input component, but the state affected by this event has to be outside because if affects all the App and will be rendered by Item's.

This is our new Input:

class Input extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: ''};                  // initially empty
    this.onChange = this.onChange.bind(this); // store input text on state
    this.addItem = this.addItem.bind(this);   // handle '+' button
  }

  addItem() {
    this.props.onAddItem(this.state.text); // call external handler
    this.setState({text: ''});             // empty the field
  }

  onChange(e){ this.setState({text: e.target.value}); }

  render() {
    return (
      <div className="Input">
        <input type="text" onChange={this.onChange} value={this.state.text}/>
        <input type="button" value="+" onClick={this.addItem}/>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

There are two events being handled here:

Input

The text input change calls onChange, similar to the toggleDone from the previous section, but in this case I store the current text from the input on the component's state attribute: text.

Add item

When you click the plus button, we read current text from the state and call this.props.onAddItem, and that props means that this is an event handler passed from outside. After that, we clear the text field to get ready for a new item.

We cannot test this just yet because we need corresponding changes outside:

The Todo Component

We need a place to put App state, and the event handler that listens to Input, but acts somewhere else:

class Todo extends React.Component{
  constructor(props){
    super(props);
    // initial state to verify rendering even before adding items
    this.state = { items: ["Example", "other"] };

    // bind the event listener, just like before
    this.addItem = this.addItem.bind(this);
  }

  addItem(value){
    // add the new item to the items list
    this.setState( { items: this.state.items.concat(value) } );
  }

  render(){
    // there is no `for` on JSX, this is how you do lists:
    const listItems = this.state.items.map((i,n) =>
      <Item key={n.toString()} text={i} />
    );

    return (
      <div>
        <h1>TO-DO</h1>
        <Input onAddItem={this.addItem}/>
        <ol>
          {listItems}
        </ol>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Pay attention to the <Input onAddItem={this.addItem}/> part on Todo's render. It is what connects Todo's addItem with Input's onAddItem.
I used different names on purpose so that it's slightly less confusing.

When you click the '+' button on Input it reads its own state.text and calls Todo's addItem which sees that text as value, and adds it to this.state.items list. By doing it with setState we tell React that Todo needs a new render.

The new render calculates listItems based on this.state.items and renders an Item component for each one of them.

To use it you need to change the call to ReactDOM.render to this:

ReactDOM.render(
  <Todo />,
  document.getElementById('app')
);
Enter fullscreen mode Exit fullscreen mode

Before click:
Ready to add item

After click:
Added item

Extra credit

Now we can add items and check them, so we are mostly done, but I want to go a bit further, so I am going to add a couple of improvements:

Remove elements:

class Item  extends React.Component {
  constructor(props){
    super(props);
    this.state = { done: false, render: true };   // store render flag
    this.toggleDone = this.toggleDone.bind(this);
    this.destroy = this.destroy.bind(this);       // new event handler
  }

  toggleDone() {
    this.setState({done: !this.state.done, render: true});
  }

  destroy(){ // set render flag to false
    this.setState({done: this.state.done, render: false});
  }

  render() {
    // returning null removes the element from DOM (but not memory!)
    if (this.state.render === false) { return null; }
    const elementStyle = (this.state.done ? {textDecoration: 'line-through'} : {});
    return (
      <li style={elementStyle}>
       <input type='checkbox' value={this.state.done} onClick={this.toggleDone} />
       <span> {this.props.text} </span>
       <input type="button" onClick={this.destroy} className='remove' value='x'/>
      </li>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

I added a new button type input to the items, and linked its click event to the destroy handler. This function just sets a new render state attribute to false, but our new render strategy returns null if that attribute is false. When a component returns null from the render function, React removes it from the DOM.

It is not removed from memory, if you examine Todo's state with your developer tools, it is still there. This could be bad in terms of performance, but good for the implementation of an "undo remove" feature. You be the judge.

Removed element

Styles

Up until now you have been looking at no more that raw html elements. However, React allows for the application of per-component styles. The way to do this is create a src/Foo.css file, and add import './Foo.css'; to your App or Component file.

If you want to know how to get to this, I leave the files below:
Final render

src/index.js

//jshint esnext:true

import React from 'react';
import ReactDOM from 'react-dom';
import './Input.css';
import './Item.css';

class Input extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: ''}
    this.onChange = this.onChange.bind(this);
    this.addItem = this.addItem.bind(this);
  }

  addItem() {
    this.props.onAddItem(this.state.text);
    this.setState({text: ''});
  }

  onChange(e){
    this.setState({text: e.target.value});
  }

  render() {
    return (
      <div className="Input">
        <input type="text" onChange={this.onChange} value={this.state.text}/>
        <input type="button" value="+" onClick={this.addItem}/>
      </div>
    );
  }
}

class Item  extends React.Component {
  constructor(props){
    super(props);
    this.state = { done: false, render: true };
    this.toggleDone = this.toggleDone.bind(this);
    this.destroy = this.destroy.bind(this);
  }

  toggleDone() {
    this.setState({done: !this.state.done, render: true});
  }

  destroy(){
    this.setState({done: this.state.done, render: false});
  }

  render() {
    // returning null removes the element from DOM (but not memory!)
    if (this.state.render === false) { return null; }
    const elementStyle = (this.state.done ? {textDecoration: 'line-through'} : {});
    return (
      <li style={elementStyle}>
       <input type='checkbox' value={this.state.done} onClick={this.toggleDone} />
       <span> {this.props.text} </span>
       <input type="button" onClick={this.destroy} className='remove' value='x'/>
      </li>
    );
  }
}

class Todo extends React.Component{
  constructor(props){
    super(props);
    this.state = { items: ["Example", "other"] };
    this.addItem = this.addItem.bind(this);
  }

  addItem(value){
    this.setState( { items: this.state.items.concat(value) } );
  }

  render(){
    console.log(`render items: ${this.state.items}`)
    const listItems = this.state.items.map((i,n) => <Item key={n.toString()} text={i} />)
    return (
      <div>
        <h1>TO-DO</h1>
        <Input onAddItem={this.addItem}/>
        <ol>
          {listItems}
        </ol>
      </div>
    );
  }
}

ReactDOM.render(
  <Todo />,
  document.getElementById('app')
);
Enter fullscreen mode Exit fullscreen mode

src/Input.css

.Input input[type=text]{
  width: 25em;
}
.Input input[type=button]{
  background-color: green;
  color: white;
  font-weight: bold;
  border: none;
  font-size: 18px;
  vertical-align: top;
}
Enter fullscreen mode Exit fullscreen mode

src/Item.css

li {
 width: 20em;
 height: 1.4em;
 box-shadow: 1px 1px 2px rgba(0,0,0,0.5);
 margin: 2px 0px;
}

li > input[type=button].remove {
  float: right;
  background-color: firebrick;
  color: white;
  border: none;
  padding: 2px 6px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;
}

li.done {
  text-decoration: line-through;
  color: grey;
}

li.pending {
  color: blue;
}
Enter fullscreen mode Exit fullscreen mode

Disclaimer

  • This is my first ever React app, it is most probably wrong
  • React recommends one js and one css file per component, I did not follow the convention for brevity
  • You can use more ES6 features or none at all, it's not imposed by the framework.

What do you think?

Was it useful to you?

Do you have any tips for me to improve?

Discussion (0)