Composing Stateless Functional Components

By Jon Leopard on 18 July 2018

When first learning React, you are introduced to the concept of components. I first learned React though the official docs, and therefore started crafting my functional components the way it taught. After spending some time in other OSS projects, as well was watching quite a few online courses and guides on React, I noticed other ways of writing these functional components.

So a couple months ago, I asked how do you write your functional components? over at spectrum.chat. The question also gained some traction over on twitter. People have a lot to say when it comes to how they write their functional components!

This was very educational and exposed me to a couple different ways to write React components. This also inspired me to write a short post on what the differences are between them. This is hardly an exhaustive blog post on the subject! I am writing this to help engrain the concepts into my head.

With that said, how you choose to write your React components is simply a matter of taste and/or whatever convention(s) your team has decided on. We'll keep it simple and only discuss functional, or (dumb) components today.


Normal Functions

As the React documentation states:

"The simplest way to define a component is to write a JavaScript function."

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

"This function is a valid React component because it accepts a single “props” (which stands for properties) object argument with data and returns a React element. We call such components “functional” because they are literally JavaScript functions."

Whats really great about React is that it makes you a better Javascript developer. Components are simply Javascript functions, there is no abstraction going on here. Lets create a simple component that we'll transform throughout this blog post:

export default function NormalFunction(props) {
  return (
    <p>
      Greetings, {props.name}. I am {props.age} years old.
    </p>
  );
}

If you are writing your component in a separate file, then you'll need to export it so that you can import it wherever you intend to use it. If you are writing a normal function declaration, you can either do this directly inline, like in the example above, or you can export at the bottom of your file like so:

function NormalFunction(props) {
  return (
    <p>
      Greetings, {props.name}. I am {props.age} years old.
    </p>
  );
}

export default NormalFunction;

When reading the React documentation, I was curious why no components were written with arrow functions (I see so many out in the wild!). Dan Abramov helped clear my curiosity:

In the docs we tend to prefer function declarations because people are more familiar with them. Hoisting is also helpful when you have many components in one file and want to define them in top-to-bottom order.

Curious what hoisting is? Here's an excerpt from the MDN docs:

Conceptually, for example, a strict definition of hoisting suggests that variable and function declarations are physically moved to the top of your code, but this is not in fact what happens. Instead, the variable and function declarations are put into memory during the compile phase, but stay exactly where you typed them in your code.

Again, another great example of how React helps you become a better Javascript developer.


Arrow Functions

We can also write React components by using ES6 Arrow Functions. For this example, we'll just do the same as the function above and write props as our single parameter. I'll explain prop destructuring in more detail in the next section.

const ArrowFunction = props => {
  return (
    <p>
      Greetings, {props.name}. I am {props.age} years old.
    </p>
  );
};

export default ArrowFunction;

We could also omit the curly braces, and remove the return statement (unique to arrow functions):

const ArrowFunction = props => (
  <p>
    Greetings, {props.name}. I am {props.age} years old.
  </p>
);

export default ArrowFunction;

Unlike our normal function, we cannot export default directly inline, so we'll have to do so at the bottom. It's also important to note that arrow function definitions are not hoisted like normal function declarations.

Another topic we should explore is function name inference. In section 7.1 of AirBNB's style guide, they suggest to use named function expressions instead of function declarations.

Why? Function declarations are hoisted, which means that it’s easy - too easy - to reference the function before it is defined in the file. This harms readability and maintainability. If you find that a function’s definition is large or complex enough that it is interfering with understanding the rest of the file, then perhaps it’s time to extract it to its own module! Don’t forget to explicitly name the expression, regardless of whether or not the name is inferred from the containing variable (which is often the case in modern browsers or when using compilers such as Babel). This eliminates any assumptions made about the Error's call stack.

If you are using @babel/preset-env to transpile your code, then const foo = () => {} will be transpiled to var foo = function foo () {}. So if thats the case, you don't have to worry much about this. However, its still good to be aware of. You can read about this in more detail here.


Prop Destructuring

In the above examples, we just simply wrote props as our function parameter which allowed us to go pass in props like props.name. Destructuring assignment is an ES6 feature and pairs nicely with stateless functional components. So, rather than declare props as our function parameter, lets destructure some props!

export default function NormalFunction({ name, age }) {
  return (
    <p>
      Greetings, {name}. I am {age} years old.
    </p>
  );
}

Of course, we can do the same with arrow functions:

const DestructuredArrow = ({ name, age }) => (
  <p>
    Greetings, {name}. I am {age} years old
  </p>
);

export default ArrowFunction;

This is great as it self documents the component and makes it clear as to what the component is expecting. The only caveat to this is that it can become ugly if you have a lot of props.


Proptypes and Flow

A way to improve your components is by adding proptypes. With React, we can add types to our components to reduce errors and make sure our props are receiving the correct type of data.

Since React v15.5, prop-types comes as a separate package that you'll need to install via yarn or npm. Implementing it is very simple, but make sure to check out the documentation.

Flow is another great tool which builds upon prop-types and adds additional power by informing you if you are misusing a component without actually running your code!

+1 for Flow!

prop-types

Here's how prop-types would look with our basic component:

import PropTypes from 'prop-types';

export default function PropTypesExample({ name, age }) {
  return (
    <p>
      Greetings, {name}. I am {age} years old.
    </p>
  );
}

PropTypesExample.propTypes = {
  // You can chain `isRequired` to make sure a warning
  // is shown if the prop isn't provided.
  name: PropTypes.string.isRequired,
  age: PropTypes.string,
}

Flow

Here's what Flow would look like, you can see its pretty similar to prop-types. The only downside to Flow is that it requires some additional set up to get running. Be sure to check out the documentation!

type Props = {
  name: string,
  age: string,
};


export default function FlowExample({ name, age }) {
  return (
    <p>
      Greetings, {name}. I am {age} years old.
    </p>
  );
}

fin

So that about wraps up what I've discovered thus far with crafting stateless functional components. I'll be implementing a codesandbox so that readers can play around with the examples I provided, so stay tuned for that. I am still learning quite a bit. If I made any errors, please mention them below in the comments!

Thanks for reading!

wq!