Learning React is much easier when you can connect the unique syntax to something you already know. In this post, I'm going to highlight a few ways in which it helps to have a mental model of React components as JavaScript functions.
What is React?
React is JavaScript library that allows you to write an enhanced HTML-style markup, known as JSX, directly within JavaScript files.
<div>
<Header />
<ContentContainer />
<Form />
<div>
How is it "enhanced"? Well, in many ways. But at the level of syntax, which is my focus here, you are able to define and use custom tags like <App />
<ContentContainer />
and <CustomerInputForm />
in addition to <div>
, <p>
, and all the rest. These "custom" tags render React components to the DOM analogous to how normal HTML tags render HTML elements.
React as HTML
This suggests a simple mental model of React components as HTML elements or maybe 'HTML++'.
That certainly seems to have been part of the inspiration for React. Indeed, someone who knew nothing about React could look at some JSX and get the idea that different entities are being rendered to the page in something like the way HTML elements are rendered.
But I hope to convince you that React components are better thought of as JavaScript functions, not HTML elements. This helps to make a number of React features more intuitive.
Starting to see "functionally"
Unfortunately, JavaScript makes this mental leap a tad difficult.
In Javascript, arguments are passed to functions by position, not name. When invoking a function, arguments must be passed in the order of the parameters in the original function definition. Order matters. The function knows what do with each argument because of where it appears in the invocation.
But in other languages, arguments can be with the name of the parameter. Then the function knows what to do with an argument because of the name it is passed with.
function greetUser(greeting, user){
console.log(`${greeting}, ${user}!`)
}
greetUser('hello', 'Sasha') // unnamed arguments
greetUser(greeting='hello', user='Sasha') // named arguments
Why is this important?
Because data or 'props' can be passed into React components using something like the HTML attribute syntax:
<CardComponent name='Sasha' role='director' />
And if you've seen functions invoked with named parameters, this is going to look a lot like:
cardFunction(name='Sasha', role='director')
It becomes easy to see an instance of a React component as a function call. Syntactically, all we've done is change up the 'punctuation' a bit, switching out angle brackets for parentheses and omitting a comma.
But if you're new to programming and you've never seen function invocations with named arguments, this might be less intuitive.
Thinking functionally
Once we're able to consistently see React components as functions, we can start to think of them as functions. Functions can serve as our base paradigm for understanding and remembering how to work with components.
Here are some ways in which this paradigm or mental model is helpful:
0: Component declaration syntax as function declaration syntax
The most obvious way in which it helps to think of components as functions is that they are declared as functions:
import React from 'react'
function MyForm(){
return (
<div>{/*Vanilla HTML or JSX referencing other
components*/}
</div>
)
}
export default MyForm
Here we are obviously using the syntax of normal JavaScript function declaration, with two slight twists: we've capitalized the first letter of the function name, and we're returning a single block of JSX.
Ok, this is a kind of trivial case. Just because something is defined like a function doesn't mean it behaves like a function. But it helps get us off on the right foot.
1: Downward data flow as function scope
In React, data or 'props' flow downward from parent components to the child components nested with them. Any variables contained in the child have to already be present in the parent.
If you model components as HTML elements, this is a bit counterintuitive. When you think of <li>
elements nested within a <ul>
, the parent doesn't know anything about the textContent
of the children. Data flow is more bottom-up.
But let's think functionally for a second. If I have a child function nested within a parent function, any variables that the child has access to have be in the scope of the parent. The child gets its context--i.e. its data--from the parent.
Thinking of components as functions makes downward flow intuitive in a way that thinking of them as HTML elements does not.
2: Prop keys as parameter names
One of the surprising things about React components is how reusable they can be. Since we can pass in event handler functions, we can make the same React component perform different actions depending on what sort of handler we pass in. Clicking on the component might delete something or add something or display an alert. It is all up to the handler that is passed in.
<MyCard onClickHandler={handleDeleteItem} />
<MyCard onClickHandler={handleAddItem} />
This might seem surprising the first time you see it.
But if you model components as functions and props as parameters, it should be a little less surprising. It would be more surprising if you couldn't do this.
The code above should be no more surprising than the below.
function myFunction(callback){...}
myFunction(cb1)
myFunction(cb2)
Just as we can pass different arguments to the same parameter, we can pass different functions as the same prop and have them executed in response to the same event in the same component.
3: Default values for props
Did you know you can set a default value for a prop that will be used in the event no value is explicitly passed to the component?
function MyComponent({ name, image = [some URL] }) {
...
}
Maybe you knew it but it seemed like quirk of React, a factoid taking up space in your brain?
If you model components as functions, this feature will seem natural. After all, function parameters in vanilla JavaScript can be given default values.
function greetUser(greeting='Hello', name){
console.log(`${greeting}, ${user}!`)
}
So if components are like functions and props are like parameters, it makes perfect sense that we are able to set default values for props.
4: Inverse data flow
Inverse data flow is one of the biggest stumbling blocks in picking up React. Downward data flow is the norm, but it sometimes happens that a variable passed down from parent to a child component is then modified in the child, after which the parent needs access to that modified state.
In React, this is done by passing a callback function as a prop from parent to child. This callback function essentially accesses the modified value in the child and assigns it to the correct variable in the parent.
OK. When I first learned React, this feature seemed very arbitrary and ad hoc. I couldn't connect it to anything that I had already encountered. It made sense as a feature of a closed React universe but not as something more general. Presumably React has a lot of sophisticated machinery under the hood, so why achieve inverse data flow with a callback?
Then I realized that you would do basically the same thing if for some reason you had nested JavaScript functions and needed to extract a value from within the child up to the parent.
Here's a somewhat contrived example that still makes the point.
function findLength(length){
length=Math.floor(Math.random()*100)
console.log(`from findLength: length=${length}`)
return `JSX from findLength`
}
function findMeasurements(){
const width = Math.floor(Math.random()*100)
let length
findLength(length)
console.log(`from findMeasurements: length=${length}`)
return 'JSX from findMeasurements'
}
findMeasurements() //=> from findLength: length=69
//from findMeasurements: length=undefined
Here we have findLength
function that generates a random length. It's using Math.random()
, but this could stand in for some complicated process or maybe a user interaction.
findLength
is called within a parent function findMeasurements
.
The question now is how we could get the value of length
up to findMeasurements
so that it can be logged in that context. findLength
determines the value of the length
variable as side effect, but it doesn't return it, so it is confined to the context of findLength
and can't be accessed by findMeasurements
.
The answer is that we could define and pass a setter function within findMeasurements
.
function findLength(length, lengthSetter){
length=Math.floor(Math.random()*100)
lengthSetter(length)
console.log(`from findLength: length=${length}`)
return `JSX from findLength`
}
function findMeasurements(){
const width = Math.floor(Math.random()*100)
let length
function lengthSetter(value){
length=value
}
findLength(length, lengthSetter)
console.log(`from findMeasurements: length=${length}`)
return 'JSX from findMeasurements'
}
findMeasurements() //=> from findLength: length=29
//from findMeasurements: length=29
We've essentially just recreated a feature of React in an entirely non-React context. React's use of callback functions to achieve inverse data flow is really no different from normal JavaScript functions would do (if they ever needed to)!
What appeared to be a strange, ad hoc feature fits nicely into our broader mental model of JavaScript functions.
Conclusion
However React components are implemented under the hood, my aim here has been to show that 'above the hood' they behave very much like JavaScript functions. We don't need to expend mental resources memorizing various React-specific features if we can see them as an extension of something we already know.
What else? The beauty of patterns and analogies is that they are open-ended. Once you have identified a few similarities, it's natural to start to look for others and deepen our understanding even further.
In what other ways does it pay to understand React components as functions?
Top comments (0)