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"
]
}
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>
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')
)
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
ore
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.
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')
);
Output:
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>;
}
}
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')
);
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:
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>
}
}
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')
);
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 });
and, if current state depends on previous state or props:
this.setState(function(state,props){
return {
// something based on previous state or props
};
})
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>
);
}
}
With this, we are able to change the state of Item
components, and React will automatically change their rendering.
Before click:
After click:
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>
);
}
}
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>
);
}
}
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')
);
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>
);
}
}
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.
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:
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')
);
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;
}
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;
}
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?
Top comments (0)