I’m going to write a really long answer to this! Sorry, I just wanted to kind of elaborate on the differences with examples in how a React app is modeled.
In Angular, you define various configurations through decorators like @NgModule
, @Component
, @Directive
, @Injectable
. They take config as parameters and the Angular framework “registers” all these decorated modules. It then runs the application centrally, with Angular behind the wheel — it reads your configs and decides when to do things like call ngOnChanges
on your component. It’s a more object-oriented and imperative style. A Node.js equivalent to me would be LoopBack: powerful core; fully-featured to make production apps quickly with only one way to do something, so there’s little inconsistency in developers’ code; much harder to master because of a huge and complex API. If you think I’m exaggerating, just read Angular’s guides for Getting Started and Component interaction; compare that with React’s Main Concepts documentation.
React’s full documentation explains this in depth, but I’ll try to summarize how “you control the library in React.” ReactDOM.render()
is a function that accepts something React knows how to render. This could be null
or false
(render nothing), a string (render a text node), or an object returned by React.createElement()
. JSX just compiles to calls to this function.
// Source codeconst MyApp = () => (
<div>
<Heading text="Hello world!" />
<Article>
It's a nice day, isn't it?
</Article>
</div>
)const Heading = ({ text }) => <h1>{text}</h1>const Article = ({ children }) => <article>{children}</article>ReactDOM.render(<MyApp />)// Compiled codeconst MyApp = () =>
React.createElement(
'div', // component type -- string for a DOM element
{},
// props (data) to pass into the component, or attributes for
// a DOM element // remaining args are "children" of the element -- these are
// actual child nodes if rendering a DOM node, or they are
// passed in as the "children" prop if the first argument to
// createElement is a function (React component)
React.createElement(
Heading,
// React will call this function with the props and render
// the elements returned from its return value { text: 'Hello world!' }
// props object to pass to the Heading function
),
React.createElement(
Article,
{},
"It's a nice day, isn't it?"
)
)const Heading = ({ text }) =>
React.createElement('h1', {}, text)const Article = ({ children }) =>
React.createElement('article', {}, children)ReactDOM.render(MyApp(), document.querySelector('#root'))
ReactDOM.render()
just receives an object—the result of React.createElement()
— and that object describes an element to be rendered. So technically, React does “call you” in the sense that React receives the component function that returns content and decides when to call it (i.e. React.createElement(Component, props, ...children)
, not React.createElement(Component({ ...props, children }))
. But this is not to invert control so much as simply because it’s practical—React can optimize your app’s performance by only re-invoking your component function when it's necessary, associating local state with a component that isn’t lost until the component is no longer rendered by its parent, and being selective about how to update the DOM based on the elements your component function returns.
I’m not here to explain React performance, because I honestly feel that performance difference is usually negligible between frameworks, as long as you adopt good design patterns.
Up until the hooks API was released in 16.8, the only way to add state to React components was by doing something that does look a bit like an Angular definition — though much, much simpler. There is only one place in React where classes are used and React controls the component by choosing when to call each method. They use specific method names to respond to events, like when the parent component re-rendered and passed a different value for a prop (componentWillReceiveProps()
) or the parent component no longer returned this element at all (componentWillUnmount()
).
With hooks, all React components are can be simple functions that return a React node. There are built-in basic hooks like React.useState()
, which return a state value and a function that can be used to replace the state value with another value, which boils down to telling the React library to: (a) associate this value with this instance of the component; and (b) call your component function again when you call the returned setter function. The core library remains simple, accepting only functions, while the functions call other functions to add behavior.
The biggest benefit to hooks is that they're easy to compose. Imagine we want different components in our app to all implement a specific behavior, which is a counter. The component that uses our behavior should:
- Hold a current count in state
- Have the ability to increment the count in response to some action
- Have the ability to reset the count to 0 if a particular prop passed into the component changes (this is a weird requirement, but I just wanted to demonstrate it)
The worst way would be to manually split out the behavior into different steps that are grouped by when they can be called in the React lifecycle.
// Sharing lifecycle behavior for a class component
// Behavior must be divided up into functions to be called into
// different functions that can be called in the component's
// various lifecyclesconst initializeCounterState = (initialCount = 0) =>
({ count: initialCount })
// Note how the component that needs to add the count behavior
// must not have a colliding property called "count" on its stateconst incrementCounter = element =>
element.setState(
({ count: currentCount }) => ({ count: currentCount + 1 })
)
// We'll need access to the React element to call its internal
// methods. If these functions are to be reused, they probably
// exist in another file. When we're debugging the Counter
// component, it won't be clear to the reader that some code
// somewhere else in the app is calling `setState` on this
// componentconst getCount = element => element.state.countconst resetCounterWhenPropChanges = (prop, element, nextProps) => {
if (nextProps[prop] !== element.props[prop]) {
element.setState({ count: 0 })
}
}class Counter extends React.PureComponent {
constructor(props) {
super(props)
this.state = {
// ...any other state needed
...initializeCounterState(props.initialCount)
}
} componentWillReceiveProps(nextProps) {
resetCounterWhenPropChanges('className', this, nextProps)
} render() {
return (
<div className={this.props.className}>
Count: {getCount(this)}
<button onClick={() => incrementCounter(this)}>+</button>
</div>
)
}
}
Since this isn’t clean, higher-order components (HOCs) solve this problem in a standard functional-programming manner. It’s just a more specific name for a HOF (higher-order function): it’s a function that accepts a React component function and returns another. The newly returned function “wraps” the original component in another one that does the dirty work and passes the data down into the original.
const withCounter = (Component, propToReset) =>
class extends React.Component {
constructor(props) {
super(props)
this.state = { count: props.initialCount || 0 }
} componentWillReceiveProps(nextProps) {
if (nextProps[propToReset] !== this.props[propToReset]) {
this.setState({ count: 0 })
}
} render() {
return (
<Component
count={this.state.count}
incrementCount={() =>
this.setState(({ count: currentCount }) =>
({ count: currentCount + 1 })
)
}
{...this.props}
/>
)
}
}const Counter = withCounter(
({ className, count, incrementCount }) => (
<div className={className}>
Count: {count}
<button onClick={incrementCount}>+</button>
</div>
),
'className'
)
Hooks are an alternative to HOCs. They don’t require adding another element to the React hierarchy, but other than that, they accomplish the same behavior. It’s simply a different model. Because it doesn’t split up behavior into lifecycle methods, it’s easier to add behavior to a component directly, like in the first example. Hooks are also composable because their internal state is entirely encapsulated. This means you can add many hooks to the same component without worrying about them conflicting with each other. And unlike HOCs, they don’t have to pass specific props into your wrapped component to work — so instead of relying on magic prop names injected by a HOC, you rely on the return values of hooks, which makes it clear that the data comes from the hook. If you use the react-redux connect()
HOC on a component, for example, you’d have to make sure that your mapStateToProps()
and mapDispatchToProps()
functions don’t return the same prop names that are passed into the component or they’ll overwrite them. If you use connect()
on a component that is already connected, you’d need to avoid collisions there too — calling the same HOC on a component twice isn’t a general pattern, whereas calling the same hook twice in a component is an encouraged pattern, which keeps your hooks light.
// Sharing behavior for a function component with hooksconst useCounter = (initialCount = 0) => {
const [count, setCount] = React.useState(initialCount) const incrementCounter = () =>
setCount(currentCount => currentCount + 1) const resetCounterWhenValueChanges = value =>
React.useEffect(() => setCount(0), [value]) return { count, incrementCounter, resetCounterWhenValueChanges }
}const Counter = ({ initialCount = 0, className }) => {
const { count, incrementCounter, resetCounterWhenValueChanges }
= useCount(props.initialCount) resetCounterWhenValueChanges(className) return (
<div className={className}>
Count: {count}
<button onClick={incrementCounter}>+</button>
</div>
)
}
When I say “easy to compose,” what I really mean is that you are writing simple, functional programming. This is function composition the native way that JavaScript is capable of. All the code that relates to a given feature that needs to be available in a component can be consolidated together. It’s completely encapsulated in a function closure. You call React.useState()
, you call setState()
. React just decides when to call your component back — and callbacks are a pattern just about every JS library uses.
Similarly, the beauty of HOCs is that they are not a React concept — they’re just HOFs. A HOF receives a function as an argument and returns a new function, which, among other things, decides when and how to call the function it received as the argument. This is not behavior driven by the React framework, which means it requires no special API to understand, and the concepts that you learn apply to all programming in the JS language.
Of course, the counterpoint is that because React doesn’t implement its own rigid way of extending and composing components, the developer may not know to use these design patterns and instead write imperative and redundant code. There are infinite bad ways to write a React application, because React only comes with barebones ReactDOM.render()
and React.createElement()
functions. It can try to decide when your components need to be updated, but it has no opinions about where your components get their information from, what they do with it, and what side effects they can produce.
React doesn’t do much out of the box — no router, no form state manager, no global state library, no dependency injector. This means that developers are free to solve these problems themselves by using other libraries. It seems cumbersome to set up a React app and get going quickly. Tools like create-react-app try to solve this by creating various templates for React projects. However, you’ll still likely need many more libraries as your app grows in complexity. The nice thing is that these libraries integrate easily with React due to its functional, compositional nature. They’re less likely to be incompatible with a different version of React, because they don’t rely heavily on a complex API.
Some modules are even compatible with other frameworks or rendering tools. You could even write your own code that compiles JSX and runs a render function of your own. The code in your React project only depends on a few functions of the React API (all of which have simple, light signatures), and if you completely replaced React with a different library that implemented the same public functions, your code is already flexible enough for that.
These are all my biased arguments for React, and it’s based on my newfound love for functional programming. That doesn’t mean Angular, Vue, or LoopBack are bad — they just have a different philosophy. They emphasize conventions and having a complete toolset to do everything you need in a way that transfers from one job or project to another. Developers will fight less about the “best way to do things” and spend less time architecting their application. Does this result in “vendor lock-in” — spending a lot of time learning specifically Angular? Yes — though, so does React. To write React effectively, you must build a React ecosystem, which means learning the APIs of many smaller packages, with the added complexity of choosing among many libraries, frameworks, and techniques that solve the same problem with different opinions.