October 12, 2015

An introduction to the development of React applications with Atom and TypeScript

We are about to develop the famous TODO App from the TodoMVC project using React and TypeScript:

In this post you will learn about the following:

1. Setting up the environment

2. Setting up the project

3. The basics about React components

4. Developing React components with TypeScript

5. Compiling the application

6. Running the application

Let’s get started!

1. Setting up the environment #

We will start by setting up the environment:

Download an install Node.js from https://nodejs.org.

Install TypeScript and tsd using npm:

$ npm install -g typescript tsd

Note: use sudo if you are using OSX

Download and install atom from https://atom.io/.

Install the atom-typescript plugin for atom:

$ apm install atom-typescript

This plugin has some cool features like HTML to TSX:

Or dependency view:

Please visit the project’s page on GitHub to learn more about the atom-typescript features.

Install the React developer tools extension for chrome.

This extension helps us to debug React applications by displaying the value of the properties and state of a selected component.

2. Setting up the project #

By the end of this tutorial the project structure will be similar to the following one:

├── index.html ├── js │ ├── app.js │ ├── app.tsx │ ├── constants.js │ ├── constants.ts │ ├── footer.js │ ├── footer.tsx │ ├── interfaces.d.ts │ ├── todoItem.js │ ├── todoItem.tsx │ ├── todoModel.js │ ├── todoModel.ts │ ├── tsconfig.json │ ├── utils.js │ └── utils.ts ├── node_modules │ ├── director │ ├── react │ └── todomvc-app-css ├── package.json ├── tsd.json └── typings ├── react │ ├── react-global.d.ts │ └── react.d.ts └── tsd.d.ts

Let’s start by creating the application’s root folder.

$ mkdir typescript-react $ cd typescript-react

Then create new package.json file inside the application’s root folder:

{ private: true, dependencies: { director: "^1.2.0", react: "^0.13.3", todomvc-app-css: "^2.0.0" } }

You can then install the project dependencies using npm:

# from the application's root folder $ npm install

This command should create a folder named node_modules inside the application’s root folder. The node_modules should contain 3 folders named: director , react and todomvc-app-css .

├── node_modules │ ├── director │ ├── react │ └── todomvc-app-css

We will now install some TypeScript type definitions files.

Type definitions files are used to declare the interfaces of the public API of third party libraries like React. These interfaces can be used by the IDEs to help us during the development of TypeScript applications with features like IntelliSense.

The type definitions files are also used by the TypeScript compiler to ensure that we are using the third party libraries correctly.

We are going to need the React type definitions. We can install them using the following command:

# from the application's root folder $ tsd init $ tsd install react --save

The command above will create a file named tsd.json and a folder named typings in the application’s root folder. The typings folder should contain a folder named react .

We also need to manually download and save a file named react-global.d.ts under the typings/react folder.

└── typings ├── react │ ├── react-global.d.ts │ └── react.d.ts └── tsd.d.ts

Now, let’s create the index.html file inside the application’s root folder:

<!doctype html> <html lang="en" data-framework="typescript"> <head> <meta charset="utf-8"> <title>React • TodoMVC</title> <link rel="stylesheet" href="node_modules/todomvc-common/base.css"> <link rel="stylesheet" href="node_modules/todomvc-app-css/index.css"> </head> <body> <section class="todoapp"></section> <footer class="info"> <p>Double-click to edit a todo</p> <p> Created by <a href="http://github.com/remojansen/">Remo H. Jansen</a> </p> <p>Part of <a href="http://todomvc.com">TodoMVC</a></p> </footer> <script type="text/javascript" src="node_modules/react/dist/react-with-addons.js"> </script> <script type="text/javascript" src="node_modules/director/build/director.js"> </script> <script type="text/javascript" src="js/constants.js"></script> <script type="text/javascript" src="js/utils.js"></script> <script type="text/javascript" src="js/todoModel.js"></script> <script type="text/javascript" src="js/todoItem.js"></script> <script type="text/javascript" src="js/footer.js"></script> <script type="text/javascript" src="js/app.js"></script> </body> </html>

At this point you should have the following files and folders in place:

├── index.html ├── node_modules │ ├── director │ ├── react │ └── todomvc-app-css ├── package.json ├── tsd.json └── typings ├── react │ ├── react-global.d.ts │ └── react.d.ts └── tsd.d.ts

You may have noticed that some of the JavaScript files referenced by our index.html file are missing. We will now proceed to solve that problem.

3. The basics about React components #

Components are he main building block of a React application. A component represents a self-contained piece of UI. A component will usually display some data and be able handle some kind of user interaction.

A component can contain child components. The application that we are about to develop is really small, so we will only develop one top-level component named TodoApp .

The TodoApp component will be composed of multiple components, including one TodoFooter component and a list of TodoItem components.

Components differentiate two different sets of data: properties and state.

Props (short for properties) are a Component’s configuration, its options if you may. They are received from above and immutable as far as the Component receiving them is concerned. A Component cannot change its props, but it is responsible for putting together the props of its child Components.

The state starts with a default value when a Component mounts and then suffers from mutations in time (mostly generated from user events). It’s a serialisable representation of one point in time—a snapshot. A Component manages its own state internally, but—besides setting an initial state—has no business fiddling with the state of its children. You could say the state is private.

When we declare a new React component using TypeScript we must declare the interface of its properties and state as follows:

class SomeComponent extends React.Component<ISomeComponentProps, ISomeComponentState> { // ... }

Now that we have our project structure in place and we know the basics about components it is time to start developing our components.

4. Developing React components with TypeScript #

Let’s create a new folder named js under the application’s root folder.

We are going to create the following files:

├── js │ ├──interfaces.d.ts │ ├── constants.ts │ ├── utils.ts │ ├── todoModel.js │ ├── footer.tsx │ ├── todoItem.tsx │ └── app.tsx

Feel free to create them now or do it as we implement each one of them.

We will use this file to define all the interfaces in our application. We use the extension .d.ts (which is also used by the type definition files) instead of .ts because this file will not be transpiled into a JavaScript file. The file is not transpiled because TypeScript interfaces are not transformed into JavaScript code during the compilation process.

// Defines the interface of the structure of a task interface ITodo { id: string, title: string, completed: boolean } // Defines the interface of the properties of the TodoItem component interface ITodoItemProps { key : string, todo : ITodo; editing? : boolean; onSave: (val: any) => void; onDestroy: () => void; onEdit: () => void; onCancel: (event : any) => void; onToggle: () => void; } // Defines the interface of the state of the TodoItem component interface ITodoItemState { editText : string } // Defines the interface of the properties of the Footer component interface ITodoFooterProps { completedCount : number; onClearCompleted : any; nowShowing : string; count : number; } // Defines the TodoModel interface interface ITodoModel { key : any; todos : Array<ITodo>; onChanges : Array<any>; subscribe(onChange); inform(); addTodo(title : string); toggleAll(checked); toggle(todoToToggle); destroy(todo); save(todoToSave, text); clearCompleted(); } // Defines the interface of the properties of the App component interface IAppProps { model : ITodoModel; } // Defines the interface of the state of the App component interface IAppState { editing? : string; nowShowing? : string }

This file is used to expose some constants. The constants are used to store the numeric value of the keyboard keys ( ENTER_KEY and ESCAPE_KEY ) that we will use later to set some events listeners.

We will also use some values to identify the currently displayed list of tasks by its status:

COMPLETED_TODOS Used when displaying completed tasks

Used when displaying completed tasks ACTIVE_TODOS Used when displaying incomplete tasks

Used when displaying incomplete tasks ALL_TODOS Used when displaying all tasks

namespace app.constants { export var ALL_TODOS = 'all'; export var ACTIVE_TODOS = 'active'; export var COMPLETED_TODOS = 'completed'; export var ENTER_KEY = 13; export var ESCAPE_KEY = 27; }

This file contains a class named Utils . The Utils class is no more that a collection of utility static functions.

namespace app.miscelanious { export class Utils { // generates a new Universally unique identify (UUID) // the UUID is used to identify each of the tasks public static uuid() : string { /*jshint bitwise:false */ var i, random; var uuid = ''; for (i = 0; i < 32; i++) { random = Math.random() * 16 | 0; if (i === 8 || i === 12 || i === 16 || i === 20) { uuid += '-'; } uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)) .toString(16); } return uuid; } // adds 's' to the end of a given world when count > 1 public static pluralize(count, word) { return count === 1 ? word : word + 's'; } // stores data using the localStorage API public static store(namespace, data?) { if (data) { return localStorage.setItem(namespace, JSON.stringify(data)); } var store = localStorage.getItem(namespace); return (store && JSON.parse(store)) || []; } // just a helper for inheritance public static extend(...objs : any[]) : any { var newObj = {}; for (var i = 0; i < objs.length; i++) { var obj = objs[i]; for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; } } } return newObj; } } }

TodoModel is a generic “model” object. Since this application is really small it may not even be worth separating this logic out, but we do this to demonstrate one way to separate out parts of your application.

/// <reference path="../typings/react/react-global.d.ts" /> /// <reference path="./interfaces.d.ts"/> namespace app.models { export class TodoModel implements ITodoModel { public key : string; // key used for local storage public todos : Array<ITodo>; // a list of tasks public onChanges : Array<any>; // a list of events constructor(key) { this.key = key; this.todos = app.miscelanious.Utils.store(key); this.onChanges = []; } // the following are some methods // used to manipulate the list of tasks public subscribe(onChange) { this.onChanges.push(onChange); } public inform() { app.miscelanious.Utils.store(this.key, this.todos); this.onChanges.forEach(function (cb) { cb(); }); } public addTodo(title : string) { this.todos = this.todos.concat({ id: app.miscelanious.Utils.uuid(), title: title, completed: false }); this.inform(); } public toggleAll(checked) { // Note: it's usually better to use immutable // data structures since they're easier to // reason about and React works very // well with them. That's why we use // map() and filter() everywhere instead of // mutating the array or todo items themselves. this.todos = this.todos.map<ITodo>((todo : ITodo) => { return app.miscelanious.Utils.extend( {}, todo, {completed: checked} ); }); this.inform(); } public toggle(todoToToggle) { this.todos = this.todos.map<ITodo>((todo : ITodo) => { return todo !== todoToToggle ? todo : app.miscelanious.Utils.extend( {}, todo, {completed: !todo.completed} ); }); this.inform(); } public destroy(todo) { this.todos = this.todos.filter(function (candidate) { return candidate !== todo; }); this.inform(); } public save(todoToSave, text) { this.todos = this.todos.map(function (todo) { return todo !== todoToSave ? todo : app.miscelanious.Utils.extend({}, todo, {title: text}); }); this.inform(); } public clearCompleted() { this.todos = this.todos.filter(function (todo) { return !todo.completed; }); this.inform(); } } }

This file uses the .tsx extension instead of the .ts extension because it contains some TSX code.

TSX is a typed superset of JSX. We will use TSX instead of HTML of client-side templates like Handlebars because TSX and JSX are used to generate an in-memory representation of the DOM. When the components state or properties change Reacts calculates the most efficient way to update the in-memory representation of the DOM and then proceeds to apply those changes to the real DOM. This process makes React highly efficient when it comes to DOM manipulation.

Note: We need use some extra compiler options to compile .tsx . We will learn more about this topic towards the end of this post.

The footer component allows users to filter the lists of tasks by their status and displays the count of tasks. This component has no state (Note how {} is passed to React.Component as the interface of its state) but it has some properties ( ITodoFooterProps ) that are set by its parent component (the TodoApp component).

/// <reference path="../typings/react/react-global.d.ts" /> /// <reference path="./interfaces.d.ts"/> namespace app.components { export class TodoFooter extends React.Component<ITodoFooterProps, {}> { public render() { var activeTodoWord = app.miscelanious.Utils.pluralize(this.props.count, 'item'); var clearButton = null; if (this.props.completedCount > 0) { clearButton = ( <button className="clear-completed" onClick={this.props.onClearCompleted}> Clear completed </button> ); } // React idiom for shortcutting to `classSet` since it'll be used often var cx = React.addons.classSet; var nowShowing = this.props.nowShowing; return ( <footer className="footer"> <span className="todo-count"> <strong>{this.props.count}</strong> {activeTodoWord} left </span> <ul className="filters"> <li> <a href="#/" className={cx({selected: nowShowing === app.constants.ALL_TODOS})}> All </a> </li> {' '} <li> <a href="#/active" className={cx({selected: nowShowing === app.constants.ACTIVE_TODOS})}> Active </a> </li> {' '} <li> <a href="#/completed" className={cx({selected: nowShowing === app.constants.COMPLETED_TODOS})}> Completed </a> </li> </ul> {clearButton} </footer> ); } } }

The TodoItem component represents one of the tasks in the list of tasks.

This component has both properties ( ITodoItemProps ) and state ( ITodoItemState ).

The component initial’s state is set in the component’s constructor by itself while the properties are passed as constructor arguments and are set by the component’s parent component (the TodoApp component).

/// <reference path="../typings/react/react-global.d.ts" /> /// <reference path="./interfaces.d.ts"/> namespace app.components { export class TodoItem extends React.Component<ITodoItemProps, ITodoItemState> { constructor(props : ITodoItemProps){ super(props); // set initial state this.state = { editText: this.props.todo.title }; } public handleSubmit(event) { var val = this.state.editText.trim(); if (val) { this.props.onSave(val); this.setState({editText: val}); } else { this.props.onDestroy(); } } public handleEdit() { this.props.onEdit(); this.setState({editText: this.props.todo.title}); } public handleKeyDown(event) { if (event.which === app.constants.ESCAPE_KEY) { this.setState({editText: this.props.todo.title}); this.props.onCancel(event); } else if (event.which === app.constants.ENTER_KEY) { this.handleSubmit(event); } } public handleChange(event) { this.setState({editText: event.target.value}); } // This is a completely optional performance enhancement // that you can implement on any React component. If you // were to delete this method the app would still work // correctly (and still be very performant!), we just use it // as an example of how little code it takes to get an order // of magnitude performance improvement. public shouldComponentUpdate(nextProps, nextState) { return ( nextProps.todo !== this.props.todo || nextProps.editing !== this.props.editing || nextState.editText !== this.state.editText ); } // Safely manipulate the DOM after updating the state // when invoking this.props.onEdit() in the handleEdit // method above. public componentDidUpdate(prevProps) { if (!prevProps.editing && this.props.editing) { var node = React.findDOMNode<HTMLInputElement>(this.refs["editField"]); node.focus(); node.setSelectionRange(node.value.length, node.value.length); } } public render() { return ( <li className={React.addons.classSet({ completed: this.props.todo.completed, editing: this.props.editing })}> <div className="view"> <input className="toggle" type="checkbox" checked={this.props.todo.completed} onChange={this.props.onToggle} /> <label onDoubleClick={ e => this.handleEdit() }> {this.props.todo.title} </label> <button className="destroy" onClick={this.props.onDestroy} /> </div> <input ref="editField" className="edit" value={this.state.editText} onBlur={ e => this.handleSubmit(e) } onChange={ e => this.handleChange(e) } onKeyDown={ e => this.handleKeyDown(e) } /> </li> ); } } }

This file contains the application’s entry point and the declaration of the TodoApp component which is the only top-level component in this application.

/// <reference path="../typings/react/react-global.d.ts" /> /// <reference path="./interfaces.d.ts"/> // We should have installed a type declaration file but // for the director npm package but it is not available // so we will use this declaration to avoid compilation // errors for now. declare var Router : any; var TodoModel = app.models.TodoModel; var TodoFooter = app.components.TodoFooter; var TodoItem = app.components.TodoItem; namespace app.components { export class TodoApp extends React.Component<IAppProps, IAppState> { constructor(props : IAppProps) { super(props); this.state = { nowShowing: app.constants.ALL_TODOS, editing: null }; } public componentDidMount() { var setState = this.setState; // we will configure the Router here // our router is provided by the // director npm module // the router observes changes in the URL and // triggers some component's event accordingly var router = Router({ '/': setState.bind(this, {nowShowing: app.constants.ALL_TODOS}), '/active': setState.bind(this, {nowShowing: app.constants.ACTIVE_TODOS}), '/completed': setState.bind(this, {nowShowing: app.constants.COMPLETED_TODOS}) }); router.init('/'); } public handleNewTodoKeyDown(event) { if (event.keyCode !== app.constants.ENTER_KEY) { return; } event.preventDefault(); var val = React.findDOMNode<HTMLInputElement>(this.refs["newField"]).value.trim(); if (val) { this.props.model.addTodo(val); React.findDOMNode<HTMLInputElement>(this.refs["newField"]).value = ''; } } public toggleAll(event) { var checked = event.target.checked; this.props.model.toggleAll(checked); } public toggle(todoToToggle) { this.props.model.toggle(todoToToggle); } public destroy(todo) { this.props.model.destroy(todo); } public edit(todo) { this.setState({editing: todo.id}); } public save(todoToSave, text) { this.props.model.save(todoToSave, text); this.setState({editing: null}); } public cancel() { this.setState({editing: null}); } public clearCompleted() { this.props.model.clearCompleted(); } // the JSX syntax is quite intuitive but check out // https://facebook.github.io/react/docs/jsx-in-depth.html // if you need additional help public render() { var footer; var main; var todos = this.props.model.todos; var shownTodos = todos.filter(function (todo) { switch (this.state.nowShowing) { case app.constants.ACTIVE_TODOS: return !todo.completed; case app.constants.COMPLETED_TODOS: return todo.completed; default: return true; } }, this); var todoItems = shownTodos.map(function (todo) { return ( <TodoItem key={todo.id} todo={todo} onToggle={this.toggle.bind(this, todo)} onDestroy={this.destroy.bind(this, todo)} onEdit={this.edit.bind(this, todo)} editing={this.state.editing === todo.id} onSave={this.save.bind(this, todo)} onCancel={ e => this.cancel() } /> ); }, this); var activeTodoCount = todos.reduce(function (accum, todo) { return todo.completed ? accum : accum + 1; }, 0); var completedCount = todos.length - activeTodoCount; if (activeTodoCount || completedCount) { footer = <TodoFooter count={activeTodoCount} completedCount={completedCount} nowShowing={this.state.nowShowing} onClearCompleted={ e=> this.clearCompleted() } />; } if (todos.length) { main = ( <section className="main"> <input className="toggle-all" type="checkbox" onChange={ e => this.toggleAll(e) } checked={activeTodoCount === 0} /> <ul className="todo-list"> {todoItems} </ul> </section> ); } return ( <div> <header className="header"> <h1>todos</h1> <input ref="newField" className="new-todo" placeholder="What needs to be done?" onKeyDown={ e => this.handleNewTodoKeyDown(e) } autoFocus={true} /> </header> {main} {footer} </div> ); } } } var model = new TodoModel('react-todos'); var TodoApp = app.components.TodoApp; function render() { React.render( <TodoApp model={model}/>, document.getElementsByClassName('todoapp')[0] ); } model.subscribe(render); render();

Make sure that the this operator is pointing to the right element at all times. For example, you should use arrow functions:

onKeyDown={ e => this.handleNewTodoKeyDown(e) }

instead of

onKeyDown={ this.handleNewTodoKeyDown }

To ensure that the this operator is pointing to the component inside the handleNewTodoKeyDown function.

5. Compiling the application #

To compile our application we must add a file named tsconfig.json under the js folder:

{ "compilerOptions": { "target": "es5", "module": "commonjs", "moduleResolution": "node", "isolatedModules": false, "jsx": "react", "experimentalDecorators": true, "emitDecoratorMetadata": true, "declaration": false, "noImplicitAny": false, "removeComments": true, "noLib": false, "preserveConstEnums": true, "suppressImplicitAnyIndexErrors": true }, "filesGlob": [ "**/*.ts", "**/*.tsx", "!node_modules/**" ], "files": [ "constants.ts", "interfaces.d.ts", "todoModel.ts", "utils.ts", "app.tsx", "footer.tsx", "todoItem.tsx" ], "exclude": [] }

If we check out the TypeScript compiler options we can find out how to use the tsconfig.json file:

The --project or -p can be used to compile the project in the given directory. The directory needs to contain a tsconfig.json file to direct compilation.

We can compile our application using the following command:

# from the application's root folder $ tsc -p js

This should create the following JavaScript files under the js folder:

├── js │ ├── app.js │ ├── constants.js │ ├── footer.js │ ├── todoItem.js │ ├── todoModel.js │ └── utils.ts

These are the files that were referenced in our index.html file:

<script type="text/javascript" src="js/constants.js"></script> <script type="text/javascript" src="js/utils.js"></script> <script type="text/javascript" src="js/todoModel.js"></script> <script type="text/javascript" src="js/todoItem.js"></script> <script type="text/javascript" src="js/footer.js"></script> <script type="text/javascript" src="js/app.js"></script>

We are now ready to run our application.

6. Running the application #

To run the application, we need a web server. We will use the npm module http-server.

We can install this package using the following commmand:

$ npm install -g http-server

Note: use sudo if you are using OSX

Use the following command to run the application:

# from the application's root folder $ http-server

If you open a browser and navigate to http://127.0.0.1:8080/ you should be able to see the application running:

Remember to open the chrome developer tools to take a look to the React developer tools extension for chrome and how the value of the properties and state of the components change as you interact with the application.

In this post we have learned how to set up a development environment and create a new project to work with TypeScript and React.

You can browse the source code online at GitHub.

Are you thirsty for more? If so, check out Typed React and Redux by Jack Hsu.

We will keep this blog updated and write more about React and TypeScript in the future. Don’t forget to subscribe if you don’t want to miss it out!

Please feel free to talk about this article with us via @OweR_ReLoaDeD and @WolkSoftwareLtd.

2,499 Kudos