The world of front-end development is evolving at a rapid pace, and it has become a challenge for web developers, both newbies and veterans alike, to keep up with this change. When dealing with front end programming, Angular and React are two popular Javascript technologies that you will come across for creating interactive single-page applications. While both are capable of helping you develop cutting-edge web and mobile applications, a question still bothers developers - which one is better?
You can find umpteen articles on the internet debating which of these technologies is a preferred option for web development. The current write-up’s objective is to give you an in-depth understanding of Angular and React, hopefully making it easier for you to choose the one that will perfectly cater to your requirements. For comparison purposes, we will use both frameworks to develop two times the identical application, and see how each of them works to decipher many of the common front end problems that you may encounter.
Angular vs. React: The Differences
There are people who argue that a comparison between Angular and React is pointless, similar to attempting a comparison between grapes and mangoes. While Angular (developed by Google) is a complete web application framework that functions fully on the client side, React (developed by Facebook) is a library that functions both on the client and server side. Even if it is possible to convert React into a complete framework by adding a few libraries, the functioning of the framework still differs entirely from that of Angular, leaving no place for a comparison in that area.
The major difference between the two technologies is found in state management. While data binding is readily available in Angular, React provides unidirectional data flow and state management by using Redux. Whether data binding is superior to the unidirectional data is another topic of hot discussion these days.
Some Angular Features not Found in React
Angular by default has some built-in features that are not found in React. These include:
-
Computed Properties
In Angular, using plain getters may not be judicious performance-wise, because they are identified on each function call. You can get around this situation by using BehaviorSubject from RsJS. In the case of React, the same objective is made possible by using @computed from MobX. -
Data Binding
Angular uses two way data binding by connecting DOM values with the Model data via the controller. So, if the user provides a new value in an input field, both the View as well as the Model gets updated. -
Dependency Injection
Angular uses a dependency injection pattern, which means the dependencies are written in a separate file. In data-binding environments, it is better to involve a little of dependency injection as it is useful for decoupling. Moreover, with Dependency Injection, it is possible to get diverse developments of various stores. -
Material Design
Material design is a widely accepted choice for many projects these days. Angular features material design components, which includes a wide range of reusable and accessible user interface components. -
Form Validation
Another feature of Angular that comes in very useful is form validations. By taking care of the validations, the library helps prevent bugs and repetition of code. -
Scoped CSS
One of the features that most developers want in their web applications is the ability to scope CSS to a particular component, without disturbing the other components. Angular supports scoped CSS which helps you achieve the CSS encapsulation.
Build the Same Application More than Once Using Angular and React
Now, let's compare the features and functions of both technologies by using them to build a simple application - a Shoutboard, which allows people pinning messages to a regular page. We will be using TypeScript as well as Apollo Client in both applications.
Routing and Bootstrapping
Given below are the entry points of both applications:
-
Angular
-
const appRoutes: Routes = [
{
path: 'home',
component: HomeComponent
},
{
path: 'posts',
component: PostsComponent
},
{
path: 'form',
component: FormComponent
},
{
path: '',
redirectTo: '/home',
pathMatch: 'full'
}
]
@NgModule({
declarations: [
AppComponent,
PostsComponent,
HomeComponent,
FormComponent,
],
imports: [
BrowserModule,
RouterModule.forRoot(appRoutes),
ApolloModule.forRoot(provideClient),
FormsModule,
ReactiveFormsModule,
HttpModule,
BrowserAnimationsModule,
MdInputModule, MdSelectModule, MdButtonModule, MdCardModule, MdIconModule
],
providers: [
AppService
],
bootstrap: [AppComponent]
})
@Injectable()
export class AppService {
username = 'Mr. User'
}
Every component used in the application should be declared, all global stores should reach providers and all third-party libraries to imports. Each of these can be accessed by Children components.
-
React
-
const appStore = AppStore.getInstance()
const routerStore = RouterStore.getInstance()
const rootStores = {
appStore,
routerStore
}
ReactDOM.render(
<
Provider { ...rootStores
} >
<
Router history = {
routerStore.history
} >
<
App >
<
Switch >
<
Route exact path = '/home'
component = {
Home as any
}
/>
<
Route exact path = '/posts'
component = {
Posts as any
}
/>
<
Route exact path = '/form'
component = {
Form as any
}
/>
<
Redirect from = '/'
to = '/home' / >
<
/Switch>
<
/App>
<
/Router>
<
/Provider >,
document.getElementById('root')
)
export class AppStore {
static instance: AppStore
static getInstance() {
return AppStore.instance || (AppStore.instance = new AppStore())
}
@observable username = 'Mr. User'
}
Since there are no module declarations, the React code is quite short. As far as bootstrapping is concerned, React is simpler as it uses just imports in the place of modules.
-
Links and Imperative Navigation
-
Angular
<
h1 > Shoutboard Application < /h1>
<
nav >
<
a routerLink = "/home"
routerLinkActive = "active" > Home < /a>
<
a routerLink = "/posts"
routerLinkActive = "active" > Posts < /a>
<
/nav>
<
router - outlet > < /router-outlet>
An active routerLink is automatically detected by the Angular Router and a proper routerLinkActive class is applied on it. The <router-outlet> element is used to provide irrespective of the chosen current path.
@Injectable()
export class FormService {
constructor(private router: Router) {}
goBack() {
this.router.navigate(['/posts'])
}
}
React
import * as style from './app.css'
// …
<
h1 > Shoutboard Application < /h1>
<
div >
<
NavLink to = '/home'
activeClassName = {
style.active
} > Home < /NavLink>
<
NavLink to = '/posts'
activeClassName = {
style.active
} > Posts < /NavLink>
<
/div>
<
div >
{
this.props.children
}
<
/div>
The class of active link is set by the React Router with activeClassName. Since the class name becomes exclusive by CSS modules compiler, it is not possible to provide the name directly.
export class FormStore {
routerStore: RouterStore
constructor() {
this.routerStore = RouterStore.getInstance()
}
goBack = () => {
this.routerStore.history.push('/posts')
}
}
Summary: In the case of routing, both Angular and React are fairly similar. While Angular appears more instinctive, React Router shows more direct approach.
Dependency Injection
Angular
@Injectable()
export class HomeService {
message = 'Welcome to home page'
counter = 0
increment() {
this.counter++
}
}
You can make any class @injectable, and its methods and properties can be accessed by components.
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
providers: [
HomeService
]
})
export class HomeComponent {
constructor(
public homeService: HomeService,
public appService: AppService,
) {}
}
<
div >
<
h3 > Dashboard < /h3>
<
md - input - container >
<
input mdInput placeholder = 'Edit your name' [(ngModel)] = 'appService.username' / >
<
/md-input-container>
<
br / >
<
span > Clicks since last visit: {
{
homeService.counter
}
} < /span>
<
button(click) = 'homeService.increment()' > Click! < /button>
<
/div>
React
import {
observable
} from 'mobx'
export class HomeStore {
@observable counter = 0
increment = () => {
this.counter++
}
}
While using MobX, a @observable decorator has to be added to any property to make it visible.
@observer
export class Home extends React.Component < any, any > {
homeStore: HomeStore
componentWillMount() {
this.homeStore = new HomeStore()
}
render() {
return <Provider homeStore = {
this.homeStore
} >
<
HomeComponent / >
<
/Provider>
}
}
In order to regulate the lifecycle properly, we should cover the HomeComponent with a Provider.
interface HomeComponentProps {
appStore ? : AppStore,
homeStore ? : HomeStore
}
@inject('appStore', 'homeStore')
@observer
export class HomeComponent extends React.Component < HomeComponentProps, any > {
render() {
const {
homeStore,
appStore
} = this.props
return <div >
<
h3 > Dashboard < /h3>
<
Input
type = 'text'
label = 'Edit your name'
name = 'username'
value = {
appStore.username
}
onChange = {
appStore.onUsernameChange
}
/>
<
span > Clicks since last visit: {
homeStore.counter
} < /span>
<
button onClick = {
homeStore.increment
} > Click! < /button>
<
/div>
}
}
Here the under-the-hood process is not only simple but also very effective in terms of performance.
Summary: In Angular, the provider lifecycle management is a built-in feature of its Dependency injection and hence, it is easier to achieve. Although the React version can also be used, it requires much more overlapping.
Computed Properties
Angular
import {
Injectable
}
from '@angular/core'
import {
BehaviorSubject
} from 'rxjs/BehaviorSubject'
@Injectable()
export class HomeService {
message = 'Welcome to home page'
counterSubject = new BehaviorSubject(0)
// Computed property can serve as basis for further computed properties
counterMessage = new BehaviorSubject('')
constructor() {
// Manually subscribe to each subject that couterMessage depends on
this.counterSubject.subscribe(this.recomputeCounterMessage)
}
// Needs to have bound this
private recomputeCounterMessage = (x) => {
console.log('recompute counterMessage!')
this.counterMessage.next(`${x} ${x === 1 ? 'click' : 'clicks'} since last visit`)
}
increment() {
this.counterSubject.next(this.counterSubject.getValue() + 1)
}
}
<
md - input - container >
<
input mdInput placeholder = 'Edit your name' [(ngModel)] = 'appService.username' / >
<
/md-input-container>
<
span > {
{
homeService.counterMessage | async
}
} < /span>
<
button(click) = 'homeService.increment()' > Click! < /button>
Each value that influences the computed property as factors should be defined as a BehaviorSubject. As you can see RxJS is used trivially only for computed properties, it is actually a drawback. Moreover, subscriptions need to be managed manually.
React
import {
observable,
computed,
action
} from 'mobx'
export class HomeStore {
import {
observable,
computed,
action
} from 'mobx'
export class HomeStore {
@observable counter = 0
increment = () => {
this.counter++
}
@computed get counterMessage() {
console.log('recompute counterMessage!')
return `${this.counter} ${this.counter === 1 ? 'click' : 'clicks'} since last visit`
}
}
As you can see, React offers a much more straightforward solution. There is a computed property that binds to counter and the result is recomputed only when counter changes. The property is referenced from the JSX template.
<Input type='text' label='Edit your name' name='username' value={appStore.username} onChange={appStore.onUsernameChange} />
<span>{homeStore.counterMessage}</span>
<button onClick={homeStore.increment}>Click!</button>
Summary: Implementation of computed properties is comparatively quick and easy in React than in Angular.
Templates and CSS
We will use the Posts component to understand how templating pile up with one another.
Angular
@Component({
selector: 'app-posts',
templateUrl: './posts.component.html',
styleUrls: ['./posts.component.css'],
providers: [
PostsService
]
})
export class PostsComponent implements OnInit {
constructor(
public postsService: PostsService,
public appService: AppService
) {}
ngOnInit() {
this.postsService.initializePosts()
}
}
This component connects and brings together HTML, CSS, and injected services. On initialization, the component also informs the function to retrieve posts from API. The use of scoped CSS prevents the content from affecting anything placed exterior to the component.
<a routerLink="/form" class="float-right">
<button md-fab>
<md-icon>add</md-icon>
</button>
</a>
<h3>Hello {{appService.username}}</h3>
<md-card *ngFor="let post of postsService.posts">
<md-card-title>{{post.title}}</md-card-title>
<md-card-subtitle>{{post.name}}</md-card-subtitle>
<md-card-content>
<p>
{{post.message}}
</p>
</md-card-content>
</md-card>
Local CSS: .mat-card { margin-bottom: 1rem; } Global CSS: .float-right { float: right; }
The global CSS is made accessible for each component. It could be expressed as class="float-right".
Compiled CSS:
.float - right {
float: right;
}
.mat - card[_ngcontent - c1] {
margin - bottom: 1 rem;
}
In compiled CSS, the local CSS is scanned to the given component, utilizing the [_ngcontent-c1] attribute selector. This helps in referencing classes normally.
React
import * as style from './posts.css'
import * as appStyle from '../app.css'
@observer
export class Posts extends React.Component < any, any > {
postsStore: PostsStore
componentWillMount() {
this.postsStore = new PostsStore()
this.postsStore.initializePosts()
}
render() {
return <Provider postsStore = {
this.postsStore
} >
<
PostsComponent / >
<
/Provider>
}
}
The Provider helps make the PostsStore dependency temporary.
interface PostsComponentProps {
appStore ? : AppStore,
postsStore ? : PostsStore
}
@inject('appStore', 'postsStore')
@observer
export class PostsComponent extends React.Component < PostsComponentProps, any > {
render() {
const {
postsStore,
appStore
} = this.props
return <div >
<
NavLink to = 'form' >
<
Button icon = 'add'
floating accent className = {
appStyle.floatRight
}
/>
<
/NavLink>
<
h3 > Hello {
appStore.username
} < /h3>
{
postsStore.posts.map(post =>
<
Card key = {
post.id
}
className = {
style.messageCard
} >
<
CardTitle
title = {
post.title
}
subtitle = {
post.name
}
/>
<
CardText > {
post.message
} < /CardText>
<
/Card>
)
}
<
/div>
}
}
Local CSS:
.messageCard {
margin - bottom: 1 rem;
}
Global CSS:
.floatRight {
float: right;
}
Compiled CSS:
.floatRight__qItBM {
float: right;
}
.messageCard__1Dt_9 {
margin - bottom: 1 rem;
}
Each CSS class comes with an arbitrary postfix by the CSS Modules loader, making it unique. It is a direct way to avoid conflicts. Webpack imported objects are then used to reference the classes.
Summary: The type checking and code completion support offered by JSX is a great feature. Although Angular uses the AOT compiler, which performs some of these tasks, it is not as perfect as the JSX/TypeScript.
GraphQL - Loading Data
We are using GraphQL to store all the application's data. A BaaS, such as Graphcool, can be used to generate GraphQL back-end easily.
Common Code
const PostsQuery = gql `
query PostsQuery {
allPosts(orderBy: createdAt_DESC, first: 5)
{
id,
name,
title,
message
}
}
`
interface Post {
id: string
name: string
title: string
message: string
}
interface PostsQueryResult {
allPosts: Array < Post >
}
Angular
@Injectable()
export class PostsService {
posts = []
constructor(private apollo: Apollo) { }
initializePosts() {
this.apollo.query<PostsQueryResult>({
query: PostsQuery,
fetchPolicy: 'network-only'
}).subscribe(({ data }) => {
this.posts = data.allPosts
})
}
}
React
export class PostsStore {
appStore: AppStore
@observable posts: Array < Post > = []
constructor() {
this.appStore = AppStore.getInstance()
}
async initializePosts() {
const result = await this.appStore.apolloClient.query < PostsQueryResult > ({
query: PostsQuery,
fetchPolicy: 'network-only'
})
this.posts = result.data.allPosts
}
}
Summary : RxJS subscribe vs. async/await are kind of the same ideas.
GraphQL - Saving Data
Common Code
const AddPostMutation = gql `
mutation AddPostMutation($name: String!, $title: String!, $message: String!) {
createPost(
name: $name,
title: $title,
message: $message
) {
id
}
}
`
Angular
@Injectable()
export class FormService {
constructor(
private apollo: Apollo,
private router: Router,
private appService: AppService
) {}
addPost(value) {
this.apollo.mutate({
mutation: AddPostMutation,
variables: {
name: this.appService.username,
title: value.title,
message: value.message
}
}).subscribe(({
data
}) => {
this.router.navigate(['/posts'])
}, (error) => {
console.log('there was an error sending the query', error)
})
}
}
React
export class FormStore {
constructor() {
this.appStore = AppStore.getInstance()
this.routerStore = RouterStore.getInstance()
this.postFormState = new PostFormState()
}
submit = async() => {
await this.postFormState.form.validate()
if (this.postFormState.form.error) return
const result = await this.appStore.apolloClient.mutate(
{
mutation: AddPostMutation,
variables: {
name: this.appStore.username,
title: this.postFormState.title.value,
message: this.postFormState.message.value
}
}
)
this.goBack()
}
goBack = () => {
this.routerStore.history.push('/posts')
}
}
Summary: There is minimal difference here as well.
Forms
The forms seen in this application are aimed at providing Data binding of fields to a model, validating messages for each field, and checking the validity of the entire form.
Angular
@Component({
selector: 'app-form',
templateUrl: './form.component.html',
providers: [
FormService
]
})
export class FormComponent {
postForm: FormGroup
validationMessages = {
'title': {
'required': 'Title is required.',
'minlength': 'Title must be at least 4 characters long.',
'maxlength': 'Title cannot be more than 24 characters long.'
},
'message': {
'required': 'Message cannot be blank.',
'minlength': 'Message is too short, minimum is 50 characters',
'maxlength': 'Message is too long, maximum is 1000 characters'
}
}
Defining the validation messages
constructor(
private router: Router,
private formService: FormService,
public appService: AppService,
private fb: FormBuilder,
) {
this.createForm()
}
createForm() {
this.postForm = this.fb.group({
title: ['',
[Validators.required,
Validators.minLength(4),
Validators.maxLength(24)
]
],
message: ['',
[Validators.required,
Validators.minLength(50),
Validators.maxLength(1000)
]
],
})
}
The FormBuilder helps to create the form shape very easily.
get validationErrors() {
const errors = {}
Object.keys(this.postForm.controls).forEach(key => {
errors[key] = ''
const control = this.postForm.controls[key]
if (control && !control.valid) {
const messages = this.validationMessages[key]
Object.keys(control.errors).forEach(error => {
errors[key] += messages[error] + ' '
})
}
})
return errors
}
onSubmit({
value,
valid
}) {
if (!valid) {
return
}
this.formService.addPost(value)
}
onCancel() {
this.router.navigate(['/posts'])
}
}
If the form is valid, data is sent to GraphQL mutation.
<h2> Create a new post </h2>
<h3> You are now posting as {{appService.username}} </h3>
<form [formGroup]="postForm" (ngSubmit)="onSubmit(postForm)" novalidate>
<md-input-container>
<input mdInput placeholder="Title" formControlName="title">
<md-error>{{validationErrors['title']}}</md-error>
</md-input-container>
<br>
<br>
<md-input-container>
<textarea mdInput placeholder="Message" formControlName="message"></textarea>
<md-error>{{validationErrors['message']}}</md-error>
</md-input-container>
<br>
<br>
<button md-raised-button (click)="onCancel()" color="warn">Cancel</button>
<button md-raised-button type="submit" color="primary" [disabled]="postForm.dirty && !postForm.valid">Submit</button>
<br>
<br>
</form>
React
export const check = (validator, message, options) =>
(value) => (!validator(value, options) && message)
export const checkRequired = (msg: string) => check(nonEmpty, msg)
export class PostFormState {
title = new FieldState('').validators(
checkRequired('Title is required'),
check(isLength, 'Title must be at least 4 characters long.', {
min: 4
}),
check(isLength, 'Title cannot be more than 24 characters long.', {
max: 24
}),
)
message = new FieldState('').validators(
checkRequired('Message cannot be blank.'),
check(isLength, 'Message is too short, minimum is 50 characters.', {
min: 50
}),
check(isLength, 'Message is too long, maximum is 1000 characters.', {
max: 1000
}),
)
form = new FormState({
title: this.title,
message: this.message
})
}
@inject('appStore', 'formStore')
@observer
export class FormComponent extends React.Component < FormComponentProps, any > {
render() {
const {
appStore,
formStore
} = this.props
const {
postFormState
} = formStore
return <div >
<
h2 > Create a new post < /h2>
<
h3 > You are now posting as {
appStore.username
} < /h3>
<
Input
type = 'text'
label = 'Title'
name = 'title'
error = {
postFormState.title.error
}
value = {
postFormState.title.value
}
onChange = {
postFormState.title.onChange
}
/>
<
Input
type = 'text'
multiline = {
true
}
rows = {
3
}
label = 'Message'
name = 'message'
error = {
postFormState.message.error
}
value = {
postFormState.message.value
}
onChange = {
postFormState.message.onChange
}
/>
<
Button
label = 'Cancel'
onClick = {
formStore.goBack
}
raised
accent
/
> & nbsp;
<
Button
label = 'Submit'
onClick = {
formStore.submit
}
raised
disabled = {
postFormState.form.hasError
}
primary
/
>
<
/div>
}
}
Summary: Both Angular and React offer a different approach to both validation and template fronts. The Angular way, although not straightforward, is actually broader and more meticulous.
Bundle Size
Angular proves bulkier with a size of 1200 KB while the bundle size of React is 300 KB.
Angular and React are both solid choices although their approach to a problem is entirely different. So, before choosing between the two, it is best to choose your programming paradigm first.
Did we help you make your choice between Angular and React? Which one did you choose, and why? Help other developers by letting them know about your choice in the comments below.