Change Is Inevitable
One of the more challenging things that I have encountered as a budding software developer is distinguishing old and outdated practices from newer, more acceptable paradigms. It is no surprise that an ecosystem as vast and complex as the modern Internet would constantly spawn new ways to do the same things better than before. It is therefore not surprising to find that the paradigm of Reactive component-based programming has shifted and morphed over the past several years into something that is, in comparison, quite concise and easier to grasp for new programmers. I'm sure that by the time I fully grasp the Reactive paradigm, I will most surely get sucked in to the newest, best, and oh-so-different way of thinking of the day. But, for now, let's take a look at the shift from class-based components to the new and improved functional components.
The Old Status Quo
If I have learned anything from my excursion into the world of web app development, it is that object-oriented programming rules. At its inception, React embraced this model at its core with the class-based component. Let's start to wrap our head around what makes the class component special by looking at the most simple example: A component that does nothing but display a single heading passed in as props:
ClassComponent.js
import React from "react";
export default class ClassComponent extends React.Component {
render() {
return (
<div className="class-container">
<h1>{this.props.text}</h1>
</div>
);
}
}
App.js
import React from "react";
import ClassComponent from "./ClassComponent";
export default class App extends React.Component {
render() {
return (
<div className="app-container">
<ClassComponent text='My First Class Component' />
</div>
);
}
}
While looking at even a simple example like this one, my newborn programmer brain squirms at understanding the full context of the syntax here. Blurry recollections of children and parent classes swim in my mind, and I can almost feel the onset of another headache that I have become accustomed to when learning new and complex concepts. While I am sure time and practice would help me to overcome this feeling of dread, I am thankful to remember that React has a new and better way of doing this with the functional component.
Less Is More
Let's take a look at the same example again, but this time made as a modern functional component:
FunctionalComponent.js
import React from "react";
export default function FunctionalComponent({text}) {
return (
<div className="functional-container">
<h1>{text}</h1>
</div>
);
}
App.js
import React from "react";
import FunctionalComponent from "./FunctionalComponent";
export default function App() {
return (
<div className="app-container">
<FunctionalComponent text='My First Functional Component' />
</div>
)
}
Not only does the functional component achieve the same result with less code, but we also avoid messing with child and parent classes and all the syntactical struggles that come with them. As the complexity increases, the addition of things such as the constructor method and the use of the this
keyword become only the beginning of the added complexity associated with the class component.
Let's layer some complexity on top of our examples to further demonstrate how these two paradigms diverge.
A Case Of Bad Parenting
Let's take a look at a solution to a very common need in react: A stateless child updating a stateful parent.
ClassParent.js
import React from "react";
import ClassChild from "./ClassChild";
export default class ClassParent extends React.Component {
constructor(props) {
super(props);
this.state = {dinner: "broccoli"};
this.changeDinner = this.changeDinner.bind(this);
}
changeDinner(newDinner) {
this.setState({
dinner: newDinner
});
}
render() {
return (
<div className="parent-container">
<ClassChild dinner={this.state.dinner} onChange={this.changeDinner} />
</div>
)
}
}
ClassChild.js
import React from "react";
export default class ClassChild extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
const dinner = event.target.value;
this.props.onChange(dinner);
}
render() {
return (
<div className="child-container">
<h1>We're having {this.props.dinner} for dinner.</h1>
<select id="dinner-options" onChange={this.handleChange}>
<option value="broccoli">
Broccoli
</option>
<option value="chicken nuggets">
Chicken Nuggets
</option>
<option value="mac n' cheese">
Mac N' Cheese
</option>
<option value="ice cream">
Ice Cream
</option>
</select>
</div>
)
}
}
Let's walk through what's happening here.
When the parent component initializes, it will run the constructor method, initializing the parent's state with a value of { dinner: 'broccoli' }
. Then we define a function that can update the value of the parent's state with a new dinner
value. Finally, we pass that information down to the child component as props. The child class then uses this information to render the heading with the current value of dinner
while also having the ability to demand something else if they don't want broccoli. And, of course, the parent will cave the child's demands for ice cream and chicken nuggets. (The child renders a dropdown that can be used to update the parent's state value for dinner
, which is subsequently passed down as props back to the child to be re-rendered in the header.)
You may also notice in the parent's constructor the mysterious line: this.changeDinner = this.changeDinner.bind(this);
. What on earth is going on there?
Remember that we want to reference the value of state of the Parent component, even when we're calling the function from inside of the Child component. If we did not include the line above, calling ChangeDinner
inside of the Child component would try to access a non-existent state value inside of itself. The binding that we do essentially makes sure that we are always referencing the state value of this particular instance of the Parent component.
While this may seem confusing, the thing to remember is that you'll want to bind any functions that are passed as an argument to another component or element. Notice that we do the same thing in the child component when we pass the handleChange
function to the select
element: we bind the function to the current instance of the child class.
There's A Better Way
I know that I could go on for even longer trying to explain what all of this means, but I for one am not a fan of how this simple example is already looking like code spaghetti. I can't imagine grasping an understanding of a complex system that utilizes this paradigm would be much fun, even if it is possible given enough time (and Tylenol). Let's refactor the example to use Functional components, and watch our lives get easier instantly.
FunctionalParent.js
import React, { useState } from "react";
import FuctionalChild from "./FunctionalChild";
export default function FunctionalParent() {
const [dinner, setDinner] = useState('broccoli');
function changeDinner(newDinner) {
setDinner(newDinner);
}
return (
<div className="parent-container">
<FuctionalChild dinner={dinner} onChange={changeDinner}/>
</div>
)
}
FunctionalChild.js
import React from "react";
export default function FunctionalChild({dinner, onChange}) {
function handleChange(event) {
const dinner = event.target.value;
onChange(dinner);
}
return (
<div className="child-container">
<h1>We're having {dinner} for dinner.</h1>
<select id="dinner-options" onChange={handleChange}>
<option value="broccoli">
Broccoli
</option>
<option value="chicken nuggets">
Chicken Nuggets
</option>
<option value="mac n' cheese">
Mac N' Cheese
</option>
<option value="ice cream">
Ice Cream
</option>
</select>
</div>
)
}
Here, we get the same result without having to worry about all the binding nonsense. The functional component just knows that we want to access and change the state value from the Parent, even if we are calling the onChange
function from the child.
Conclusion: Don't Make Things Harder Than They Have To Be
I find that I am constantly struggling against complexity in favor of simplicity. It is so easy to drink the Kool-Aid and believe that things must always be nastily complicated all the time. The truth is that people are constantly coming up with new and easier ways to get the job done, and it would be foolish not to take advantage of new technology that makes it possible to become a software engineer without a PHD in computer science. The time and energy saved by utilizing new approaches can be redirected and used to do what I actually want to do: design and build things that will make a difference in the world (aka, make me a millionaire).
Read more about functional and class components here.
Top comments (0)