Back to Blog

How React.createElement() works under the hood

Wednesday, March 17, 2021

Photo by Lenny Kuhne

After I learn how to develop a frontend using React in Epic React, I want to be able to contribute to React. So I started taking the first steps to be able to contribute, which was understanding the code. I start by understanding React.createElement function. When we call React.createElement, there are many things that happen, so I write it here to remind me later and hopefully can also help other people that want to start to contribute too. Here are the things that happen when we call React.createElement :

1. Validating the type of element

The first thing that happens when we call React.createElement is validating the element type. This only happens in the development environment. Here is where it happens at the time I write this article.

We pass the element type as the first argument of React.createElement

1// h1 is the element type
2React.createElement('h1', { children: "Hello World" })

in that example, h1 is the type of element that we create. There is a couple of types that will be considered valid :

1.1. String

If we pass a string as an element type, it will be considered as a valid type. We use string as a type if we want to create a pure HTML element like h1.

1React.createElement('h1', { children: "Hello World" })

1.2. Function

We can also pass a function as the element type. We use a function as a type if we want to create a custom component. The function that we pass must return something that renderable, for example, a string, number, or another React element

1const Heading = () => React.createElement('h1', { children: "Hello World" })
2
3// notice that Heading is a function
4React.createElement(Heading)

In the above example, we create a custom component called Heading, then we create an element using that custom component. Notice that Heading that we pass as a type is a function

1.3. Symbol

In React, a symbol is used for tagging React element-like types, such as Fragment, StrictMode, Profiler, etc. So if we pass a known symbol as element type, it will be considered as valid

1// React.Fragment is a symbol
2React.createElement(React.Fragment, { children: "Hello World" })
3
4// We usually use JSX instead
5<React.Fragment>Hello World</React.Fragment>

1.4. Object

There are some cases that we might pass an object as a type. For example, if we wrap our component using React.memo to memoize the component.

1const Heading = () => React.createElement("h1", { children: "Hello world" })
2
3// React.memo will return an object
4const MemoizedHeading = React.memo(Heading)
5
6console.log(MemoizedHeading) // { type: function, $$typeof: Symbol.for("react.memo") }
7
8// MemoizedHeading is an object
9React.createElement(MemoizedHeading)

If React.createElement receive an object as a type, it will check if there is a $$typeof property that contains a symbol. Then, it will check if the symbol is known. If it a known symbol, then the type is considered valid

Other than the types mentioned above, it will be considered as invalid, for example, null or undefined. React will show this warning if the type that we pass is invalid

Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: null.

2. Creating the React element

After validating the type of element, React will create the element object, but before it, there is a couple of things that happen:

2.1. Moving children arguments to props object

There 2 ways to add children into the element using React.createElement. The first way is by inserting the children into props objects. The second way is passing the children as the third argument of React.createElement

1// insert children into props object
2React.createElement("div", { children: ["children 1", "children 2", "children 3"] })
3
4// pass children as the third argument
5React.createElement("div", null, "children 1", "children 2", "children 3")

If we pass children as the third argument of React.createElement, it will be moved to the props object under the hood

1// insert 3 children into div element
2const element = React.createElement(
3    "div", 
4    { id: "container" }, 
5    "children 1", 
6    "children 2", 
7    "children 3"
8)
9
10
11// that 3 children is moved to props object
12console.log(element) 
13/* 
14{ 
15    type: "div", 
16    props: {  
17        id: "container",
18        children: [ "children 1", "children 2", "children 3" ] 
19    } 
20}
21*/

If we pass children on props object and as the third argument, the children in the third argument will be prioritized

1const element = React.createElement(
2    "div", 
3    // pass children inside props object
4    { children: ["children A", "children B", "children C"] }, 
5    // also, pass children as third argument
6    "children 1", 
7    "children 2", 
8    "children 3"
9)
10
11// the children that we pass in props object is replaced
12console.log(element) 
13/*
14{ 
15    type: "div", 
16    props: {  
17        children: [ "children 1", "children 2", "children 3" ] 
18    } 
19}
20*/

Here is where it happens at the time I write this article

2.2. Set default props value

We can add default props to our component by setting the defaultProps property, React will move the value in defaultProps to props

1class Message extends React.Component {
2    render (
3        <div>{this.props.title}</div>
4    )
5}
6
7// set the default props
8Message.defaultProps = {
9    title: "Welcome"
10}
11
12// create React element without passing props value
13const message = React.createElement(Message)
14
15// the props is filled by the value in default props
16console.log(message) // { type: function, props: { title: "Welcome" } }
17
18// create React element with props
19const message2 = React.createElement(Message, { title: "Hey yo" })
20
21// the default props will be ignored
22console.log(message2) // { type: function, props: { title: "Hey yo" } }

Here is where it happens at the time I write this article

2.3. Set warning when accessing key props

We will get a warning if we access the key props inside a component

1const ListItem = (props) => {
2    // we are accessing the key props here, this will trigger warning
3    console.log(props.key)
4    return <div>{props.children}</h1>
5}
6
7const List = () => {
8    const items = ["eat", "sleep", "repeat"]
9    return (
10        <div>
11            {items.map((item) => (
12                // giving the key props
13                <ListItem key={item}>{item}</ListItem>
14            ))}
15        </div>
16    )
17}
18
19React.createElement(List)

React do this by modifying the access of key property on props object by adding a warning function. So whenever we access the key property, it will show this warning

Warning: ListItem: key is not a prop. Trying to access it will result in undefined being returned. If you need to access the same value within the child component, you should pass it as a different prop.

Here is where it happens at the time I write this article

2.4. Set warning when accessing ref props

We will also get a warning if we access the ref props inside a component

1const Message = (props) => {
2    // we are accessing the ref props here, this will trigger warning
3    console.log(props.ref)
4    return <h1>{props.children}</h1>
5}
6
7const Wrapper = () => {
8    // create the ref
9    const ref = React.useRef()
10    return (
11        <div>
12            {/* passing the ref */}
13           <Message ref={ref}>Hello Everyone</Message>
14        </div>
15    )
16}
17
18React.createElement(Wrapper)

React do this by modifying the access of ref property on props object by adding a warning function. So whenever we access the ref property, it will show this warning

Warning: Message: ref is not a prop. Trying to access it will result in undefined being returned. If you need to access the same value within the child component, you should pass it as a different prop.

Here is where it happens at the time I write this article

2.5. Creating the React element

After doing a couple of things before, React will create the React element. React element is an object that contains the following properties

1{
2    $$typeof,
3    type, 
4    key, 
5    ref,
6    props,
7    _owner
8};

React will also add $$typeof property that has value Symbol.for('react.element'). This helps React for securing from injection attack. The React element object will also have _owner property for recording who is the parent of that element. Here is where React create the React Element at the time I write this article

3. Validate child key

After React create the React element object, then we will move to the next step, validating the children inside the props object. Here is where it happens when I write this article. This step is consist of two smaller step

3.1. Validate is the children is a valid element

React will validate if the children are a valid element. It will be considered as valid if the children is an object that has a $$typeof property with value Symbol.for('react.element').

3.2. Validate is the children have key props

If the children are a valid element, React will check if the children have key props. If the children don't have key props, React will log this warning in the console

Warning: Each child in a list should have a unique "key" prop.

4. Validating fragment props

If the element type that we create is a fragment, it will check the props. A fragment may only have children and key props on it.

1// this will show a warning
2React.createElement(React.Fragment, { title: "tes" });

If we pass other props except children and key, React will log this warning in the console

Warning: Invalid prop title supplied to React.Fragment. React.Fragment can only have key and children props.

Here is where React validate the fragment props at the time I write this article

5. Validate props type

If the element type is not a fragment and we set the propTypes property on the component, React will validate the props value and log a warning if the type of value is not suited with propTypes

1class Greeting extends React.Component {
2  render() {
3    return <h1>Hello, {this.props.name}</h1>;
4  }
5}
6
7// set the propTypes
8Greeting.propTypes = {
9  name: PropTypes.string
10};
11
12// give invalid value to name props
13React.createElement(Greeting, { name: 5 });

Warning: Failed prop type: Invalid prop name of type number supplied to Greeting, expected string.

Here is where React validate the props type when I write this article

There are so many things that happen under the hood when we call React.createElement. I could have missed something, if I do, I will add it later when I more familiar with the codebase. Hopefully, this can help me to understand the code base and help you too, thanks