React vs other frameworks: Creating a simple app

React.js is a popular framework for creating web applications. It is rivalled by other frameworks. Most notably Vue.js and AngularJS/Angular 2+. While React has some unique concepts and convenient features, other frameworks have their own advantages. In this post I am going to create a simple application using React and compare the process with creating an application with other frameworks. If you are deciding which framework to choose for your development stack, or if you are already working with React, but thinking about switching to another framework, or add some other framework to your projects this post may be helpful for you.

We are going to use create-react-app to create our starting application. It is analogous with Vue CLI and Angular CLI, but used for React applications. Create-react-app is powered by webpack.

Install create-react-app if you don’t have it yet.

npm install -g create-react-app

Let’t create an app with create-react-app.

Create-react-app event_admin

Our project directory has the following structure:

.gitignore
README.md
node_modules
package.json
public
src
yarn.lock

Here node_modules folder contains modules that are necessary to develop our app, package.json is the configuration for our project, public/ folder will contain static assets for our app, which will eventually be copied by webpack to a build/ folder (Actually React encourages to import static files as modules in your files, but if you have a certain usecase, this can be helpful for you). This folder is similar to static/ folder that is created by Vue.js’s CLI, Vue also has an assets folder in src/, but that folder is just for organizing assets, imported as modules, you don’t have to use it if you don’t want to use it. In Angular 2+ there is an assets/ folder that is actually inside the src/ folder, and you can easily configure which folders can also hold static files and which files are static, this assets/ folder is an analog of React’s public/.

The code for our application is located in src/ folder which has the following structure:

├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
└── registerServiceWorker.js

We will need routing for our application. Let’s Install react-router. React router has been changing api over the last 3 versions. We are going to use version 4 and this is the way it gets installed:

npm install react-router-dom —save

Or

yarn add react-router-dom

We also are going to use material-ui library for styling. So let’s install it too.

npm install material-ui —save

Or

yarn add material-ui

Ok, we are all set. Let’s start writing the code for our application.

We are using Material-UI library, so let’s put the code needed for it to function in our app. We will do it in a main entry file of our application – index.js. It is already created for us by create-react-app, so all we need is just add material-ui code:

import React from 'react';
import ReactDOM from 'react-dom';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

// If you are going to use Redux for state management, and you are going to use
// the official React bindings for Redux you may wrap the App component in
// Provider component from react-redux
const Bootstrap = () => (
  <MuiThemeProvider>
    <App />
  </MuiThemeProvider>
);

ReactDOM.render(<Bootstrap />, document.getElementById('root'));
registerServiceWorker();

We are importing MuiThemeProvider component and wrap the App component in it.

I want to say a couple words about css in create-react-app. CSS styles are not scoped, which means styles in one component can override styles with the same selector in another component. You can choose to use css modules to get scoped css, but you must eject from create react app(npm run eject), which means that create-react-app will not manage your app anymore, you have to do everyting yourself. In Angular and Vue you have styles scoped to a component with CLI by default (in Vue you add scoped attribute to a <style> tag in .vue files), which is nice.

Our application is going to feature a list of users on one page and an ability for an admin to edit a particular user on another page. Since we have two types of pages, we are going to define routes with the help from react-router.

In Vue and Angular you define your application routes via configuration objects, which are then passed using various mechanisms to the rest of the application. React-router also has this option, but there is another way to define and instantiate routes, which is more common: via JSX.

If you don’t know what JSX is, I will tell you. JSX is a way to declaratively describe the component structure of an application. It is similar to templating language, but it is not quite the same. A component’s markup gets rendered based on JSX.

  As I mentioned earlier, react router has been changing the way it defines routes, and since we are using version 4 of it, here is how we are going to define them using JSX:

<Router>
  <div>
    <Route exact path="/" component={UserList}/>
    <Route path="/user/:id" component={UserDetail}/>
  </div>
</Router>

Here we have 2 route definitions. When a user navigates to a URL that is matched by one of the path attributes of a Route component, the router tells React to render a corresponding component(for example UserList) from a component attribute. The `exact` attribute tells the router to match the exact value inside quotes, otherwise it would match routes that contain not only the backslash, but also other characters.

We are going to put the routes in our App component, which has already been generated for us by create-react-app. We need to replace its contents with our logic:

import React from 'react';
import {
  BrowserRouter as Router,
  Route
} from 'react-router-dom'
import './App.css';
import UserList from './components/UserList';
import UserDetails from './components/UserDetails';

const App = () => (
  <Router>
    <div>
      <Route exact path="/" component={UserList}/>
      <Route path="/user/:id" component={UserDetails}/>
    </div>
  </Router>
)

export default App;

We use a functional component here (represented by a function rather than a class), because our component has no internal logic, it just renders its contents. It imports two components. It also imports two components, that represent the routes.

Right now the application will throw an error if you try to run it because the imported modules do not actually exist, so we need to create some sorts of placeholder components to check if our routing works correctly. React is not as nice as Angular in regards to creating a basic component. You have to do everything by hand, or at least use snippets your in text editor/IDE. Angular on the other hand has a scaffolding tool provided by CLI, that creates a needed component and all the accompanying files with boilerplate.

Let’s create the needed components in components folder with this structure

├── components
│   ├── UserDetails
│   │   ├── index.js
│   │   └── style.css
│   └── UserList
│       ├── index.js
│       └── style.css

A placeholder component for user details should look like this (located in index.js of the corresponding folder):

import React, { Component } from 'react';
import './style.css';

const UserDetails = () => (
  <div>User Details</div>
)

export default UserDetails;

Create another one for a user list

import React, { Component } from 'react';
import './style.css';

const UserList = () => (
  <div>User List</div>
);

export default UserList;

We use index.js for the component files, to make imports statements shorter, but you can name them after the component, this will make debugging in chrome devtools easier, as every tab in debugger will be named after the component rather than index.js, also stack traces will be unique for a component.

Normally you would get users from a database, but for our example application we can get mock users. Let’s add them in a mock_data.js file. Mock data will be of the following format:

export default [
  {
    "id": 1,
    "first_name": "Janos",
    "last_name": "Fawks",
    "email": "jfawks0@cbc.ca",
    "gender": "Male",
    "Avatar": “JustoSit.png”,
    “age”: 21
  },
  …
]

Lets work on our UserList component. You should edit it to look like this:

import React, { Component } from 'react';
import './style.css';
import UserListItem from '../UserListItem';
import users from '../../mock_data';

// Consider switching to PureComponent, when performance becomes an issue in your app
class UserList extends Component {

  constructor(props) {
    super(props);
    this.state = {
      users: []
    };
  }

  componentDidMount() {
    setTimeout(() => {
      this.setState({ users });
    })
  }

  render() {
    return (
      <ul id="user-list">
        {
          this.state.users.map(user =>
          <li key={user.id} className="user-list-item">
            <UserListItem {...user} />
          </li>)
        }
      </ul>
    );
  }
}

export default UserList;

We are going to import UserListItem, which we will create later. We need to import mock user data here. In a real app you will use some http library to get the users data from the real server (some of the popular choices are fetch and axios). The recommended method to fetch data in React is in componentDidMount – it is a React lifecycle method, Vue and Angular also.have lifecycle methods.

We define the component’s state in the constructor. You usually define a state in a constructor as a property of a component. In Vue’s components the role of state is taken by a data property of an initialization object, and in angular you usually define state properties as properties of a component’s class, or use ngModel. In React changing a component’s state requires calling a method called setState. In Vue or Angular, the state is updated in response to a change of property value, the latter in my opinion is more convenient.

When React component has mounted, componentDidMount is called and we are going to simulate asynchrony by using setTImeout function. Once the setTimeout callback gets called we call setState with an object, containing our mock users. Then the render method gets called. And we get our list of users. We iterate over the list of users using map and render a <li> with UserListItem component. We use object spread here {…user} which will pass every key-value pair of a user object as a separate prop to a child component.

So, we took care of the UserList component, now let’s work on the UserListItem component, which will render user cards. This is the code for the component:

import React from 'react';
import { Link } from 'react-router-dom';
import { Card, CardActions, CardMedia, CardTitle } from 'material-ui/Card';
import FlatButton from 'material-ui/FlatButton';

// Consider switching to PureComponent, when performance becomes an issue in your app
const CardExampleWithAvatar = ({ id, Avatar, first_name, last_name }) => (
  <div className="user-list-item">
    <Card>
      <CardMedia style={{
        width: '400px',
        height: '400px',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        background: '#eee'
       }}>
        <img src={Avatar} alt={`${first_name} ${last_name}`} />
      </CardMedia>
      <CardTitle
        title={`${first_name} ${last_name}`}
        style={{ height: '72px', display: 'flex', alignItems: 'center' }}
      />
      <CardActions style={{ display: 'flex' }} >
        <Link to={`/user/${id}`}>
          <FlatButton label="View Details" />
        </Link>
        <FlatButton label="Action2" />
      </CardActions>
    </Card>
  </div>
);

export default CardExampleWithAvatar;

This component makes heavy use of Material UI components. You can see which ones by looking at material ui imports. We use Card component, with its accompanying components and FlatButton component.

We use object destructuring inside function parameters in this functional component. It is a feature of ES6 which is often used in React. Every property listed will be available by its name inside the function, rather than through the dot notation.

Since in this demo application we put our mock images in public folder, they will be available from the root path of the application.We set user avatar, name and create user action buttons. One of them takes you to that particular user’s detail page. You see it is wrapped in Link component with to attribute. That component tells react-router where to navigate, it renders a <a> element. We use a user’s id to create a route path.

Ok, now its time to create the user details component. It will have the following code:

import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import TextField from 'material-ui/TextField';
import RaisedButton from 'material-ui/RaisedButton';
import Snackbar from 'material-ui/Snackbar';
import './style.css';
import users from '../../mock_data';

class UserDetails extends Component {

  constructor(props) {
    super(props);
    this.state = {
      user: null,
      statusMessage: false
    };
    this.saveChanges = this.saveChanges.bind(this);
  }

  componentDidMount() {
    // In a real app you query your server for a user with particular ID via,
    // some http library
    setTimeout(() => {
      const user = users.find(user => user.id === +this.props.match.params.id);
      this.setState({ user });
    });
  }

  saveChanges(event) {
   event.preventDefault();
    const inputs = event.target.getElementsByTagName('input');
    Array.prototype.forEach.call(inputs, element =>
      console.log(`saving ${element.name} with value ${element.value}`));
    this.setState({ statusMessage: true });
  }

  render() {
    if (!this.state.user) return null;
    const { Avatar, first_name, last_name, age, email } = this.state.user;
    return (
      <div id="user-details-container">
        <Link to="/" id="user-details-home-link">
          <RaisedButton label="User List" />
        </Link>
        <form onSubmit={this.saveChanges} id="user-details-form">
          <div id="user-details-avatar">
            <img src={`/${Avatar}`} alt={`${first_name} ${last_name}`} />
          </div>
          <TextField
            name="first_name"
            hintText="Enter first name"
            floatingLabelText="First Name"
            defaultValue={first_name}
          />
          <TextField
            name="last_name"
            hintText="Enter last name"
            floatingLabelText="Last Name"
            defaultValue={last_name}
          />
          <TextField
            name="email"
            hintText="Enter email"
            floatingLabelText="Email"
            defaultValue={email}
          />
          <TextField
            name="age"
            hintText="Enter Age"
            floatingLabelText="Age"
            defaultValue={age}
          />
          <div id="user-details-button-container">
            <RaisedButton
              label="Save"
              primary={true}
              type="submit"
              style={{ marginRight: '12px' }}
            />
          </div>
        </form>
        <Snackbar
          open={this.state.statusMessage}
          message="User details saved"
          autoHideDuration={4000}
        />
      </div>
    );
  }
}

export default UserDetails;

Wow, there are a lot things going on here. Don’t worry I will explain everything in detail. Again we import a Link component from react-router. We use RaisedButton for buttons, TextField for form inputs, and Snackbar for a notification when an admin submits an edited user. These all come from Material UI library. And we are also going to use the same mock user data that we used in user list item component.

In a component’s constructor we initialize the state object, which will hold a user’s data once it’s available to our component, the state will also contain a flag that tells the Snackbar component to render a toast message at the bottom of the screen, when a user details are saved. We also bind the saveChanges method in the component’s constructor. It gets called when a user submits the form.

When a component is initialized, it needs a user data to show. In our demo application we are going to use a mock user data, as opposed to real data received through http in a real application. So in componentDidMount we again call setTimeout to simulate asynchrony, we find a relevant user, whose id we extract from the URL by means of react-router’s match.params object, which gets passed to the component with its props.

const user = users.find(user => user.id === +this.props.match.params.id);

Then we update the state with a user data.

this.setState({ user });

We are using es6’s object property shorthand here which is equivalent to { user: user }.

In a render method we need to first check if state.user.id has been set, because our asynchronous request for the user data may have not been completed yet.

if (!this.state.user) return null;

Then we extract the necessary user properties via an object destructuring and return the JSX for the user details. We use TextInput for rendering the form’s inpus. After a user saves changes Snackbar shows a notification.

The form’s submit event calls the saveChanges method. While you can specify a callback inside the onSubmit property, it is not a good practice in React applications, because function declaration creates a new function. Whenever a component whose prop value is that function declaration will receive that prop change(new function is a change from old function) on UserDetails render and the form component will be rerendered, which affects the performance. The most common solution in React is to pass a reference to a method of a component in whose render method that JSX is located. In our case we have a form component, which gets passed a reference to this.saveChanges method of UserDetails component, the component that contains the render method.

Passing a component’s method has its caveats though. Because the method is passed as a callback, it will not refer to its containing component as ‘this’, to fix that we need to either use an arrow function when defining that method, or bind this method to the component in the component’s constructor. Using arrow method is possible but it is not a default Babel configuration in most cases. So most commonly people use method binding. That is what we use here.

this.saveChanges = this.saveChanges.bind(this);

In saveChahges we have to call event.preventDefault() to prevent the form’s submit and a page refresh. We have a single page application so a refresh is not desirable for us. In compoarison, in Vue there is a nice syntactic sugar to get rid of this call in every method, in a template you just specify .prevent, after an attribute name.

Then we get all the inputs in a form in a form of HTMLCollection, to iterate over them we use Array.prototype.forEach.call to handle the iteration in functional programming style (HTMLCollection is not an array and doesn’t have its own forEach method, so you have to use for loop for iteration). Finally we just log every saved user property to the console, because it is a demo app. In a real app you would collect the values and send them to a server via http. After that we update the state to reflect the saved changes and show the message at the bottom.

That is pretty much all the logic for our application. Right now you have some idea how to create a simple application in React and how the process compares to creating an application in other frameworks. The complete code for this post’s app can be found on Github: https://github.com/jsmegatools/creating-a-simple-app-with-React.js

Leave a Reply

Your email address will not be published. Required fields are marked *