Hi! 👋 I'm Juan, I have more than 6 years of experience working with React, and I think it's not that hard. For that reason, I've decided to create a series that will teach you React from 0, and if you already know React, it will help you go to the next level. We'll talk about really basic things in depth and really advanced topics. My objective at the end of this series is to give you enough context and knowledge so you can create your own React project, like a library.
What are we going to learn in this series?
- How to build a React library the easy way
- Interesting React helpers and React methods
- Strange exports that can make your code more reusable
- File systems to organize your project
- Obviously, how to build a React project
- Much more...
What level do you need to start?
Do you know how to write code in any language? Pay attention to my words: "Know how to write code." I didn't say: "How to write good code." If the answer is yes, you are good to go. You'll start to get it sooner or later. And if you do not, leave a message with any doubt you have.
With all of this said, we can finally start.
What's JS and why is it amazing?
JS is one of the greatest pieces of art ever created. It is a single-threaded, cross-platform, easy-to-use, and understandable programming language. It was primarily designed to run code on the web, inspired by its bigger brother C#, sharing many syntactical aspects (they have similar writing). It's really helpful for projects that are just starting because, thanks to the use of an engine that implements an interpreter and a compiler, JS can run almost anywhere: your phone, your computer, your smartwatch, and your browser.
Playing with the browser
I've mentioned that JS was originally intended to run on the web and indeed, it is one of the fundamental pieces of the modern web that we all enjoy these days. Let me show an example of JS running in the browser:
// Function to get the window size and set it into the <p> tag
function updateWindowSize() {
const width = window.innerWidth;
const height = window.innerHeight;
const sizeText = `Width: ${width}px, Height: ${height}px`;
// Get the <p> tag by its id
const windowSizeElement = document.getElementById('window-size');
// Set the window size text into the <p> tag
windowSizeElement.textContent = sizeText;
}
// Update window size on load
window.addEventListener('load', updateWindowSize);
// Update window size on resize
window.addEventListener('resize', updateWindowSize);
Here we can see a function that gets the size of the window and sets it into a <p>
tag. This is something that until recent times was only possible with JS. This is because this was the only code that was able to run on the client side.
Besides accessing the browser properties like the Window Object, JS also can modify the DOM and add elements. That enables the possibility of dynamically rendering different parts of the application. Let me show you:
// Function to render a form with fields: name, last name, and password
function renderForm() {
// Create form element
const form = document.createElement('form');
// Create name field
const nameLabel = document.createElement('label');
nameLabel.textContent = 'Name: ';
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.name = 'name';
nameInput.id = 'name';
nameLabel.appendChild(nameInput);
form.appendChild(nameLabel);
form.appendChild(document.createElement('br')); // Line break for better layout
// Create last name field
const lastNameLabel = document.createElement('label');
lastNameLabel.textContent = 'Last Name: ';
const lastNameInput = document.createElement('input');
lastNameInput.type = 'text';
lastNameInput.name = 'last_name';
lastNameInput.id = 'last_name';
lastNameLabel.appendChild(lastNameInput);
form.appendChild(lastNameLabel);
form.appendChild(document.createElement('br')); // Line break for better layout
// Create password field
const passwordLabel = document.createElement('label');
passwordLabel.textContent = 'Password: ';
const passwordInput = document.createElement('input');
passwordInput.type = 'password';
passwordInput.name = 'password';
passwordInput.id = 'password';
passwordLabel.appendChild(passwordInput);
form.appendChild(passwordLabel);
form.appendChild(document.createElement('br')); // Line break for better layout
// Create submit button
const submitButton = document.createElement('button');
submitButton.type = 'submit';
submitButton.textContent = 'Submit';
form.appendChild(submitButton);
// Append the form to the container
const formContainer = document.getElementById('form-container');
formContainer.appendChild(form);
}
// Call the function to render the form
renderForm();
This is a function that is able to render a form on the screen, but probably you are thinking: "Man!!! This is huge" yep, it is. In fact, this function has 47 lines of code that are not easy to read.
How's that happening?
If you are something like me, probably you are asking yourself: "How can JS change the DOM?" or even "What's the DOM?" Let's be honest, nobody is asking these questions, but stay with me. Those are great questions, let me show you:
This tree-like structure is the skeleton of our page, and JS is naturally prepared to interact with it.
It's not that good
We’ve been seeing how amazing JS is and all the power that it has, but there's a caveat. It's not that good. As we already have seen, the syntax to do modifications on the DOM is kind of hard to read, easy to break, and difficult to scale into bigger ventures. Not to mention the HORRIBLE performance that comes with directly manipulating the DOM.
Each time you make a modification to the DOM, it triggers a re-rendering on the screen, which also produces recalculation of the sizes of the elements, checking on the style sheets all over again, and it’s really hard to tell the possible side effects of some modifications. For that reason, directly manipulating the DOM at a bigger scale can be really complex.
If you want to know more about it, here is an excellent resource: Performance considerations when manipulating the DOM.
What's React and why is it amazing?
React is a library—mark my words: "A library", not a framework nor magic, Library. Great, now that we’ve made that clear, we can continue. React was created as a solution for the problems previously mentioned, trying to implement a solution that is easy to use, understand, and grow in order to create more sustainable and complex applications on the client side. But what does that mean? It means that React is easy, scalable (I might disagree with this one), and performant (Yes and no, we’ll understand it later).
Let's see React in action:
import React from 'react';
const FormComponent = () => {
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
};
return React.createElement(
'form',
{ onSubmit: handleSubmit },
React.createElement(
'label',
null,
'Name:',
React.createElement('input', {
type: 'text',
name: 'name'
})
),
React.createElement('br', null),
React.createElement(
'label',
null,
'Last Name:',
React.createElement('input', {
type: 'text',
name: 'lastName',
})
),
React.createElement('br', null),
React.createElement(
'label',
null,
'Password:',
React.createElement('input', {
type: 'password',
name: 'password',
})
),
React.createElement('br', null),
React.createElement(
'button',
{ type: 'submit' },
'Submit'
)
);
};
export default FormComponent;
This ugly piece of art is React creating the same form that we were seeing just before in plain JS. And I know, I know, many of you out there may be saying that this is not React and that React should look something more like this:
import React from 'react';
const FormComponent = () => {
const handleSubmit = (e) => {
e.preventDefault();
// Handle form submission
console.log('Form submitted:', formData);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input
type="text"
name="name"
onChange={handleChange}
/>
</label>
<br />
<label>
Last Name:
<input
type="text"
name="lastName"
onChange={handleChange}
/>
</label>
<br />
<label>
Password:
<input
type="password"
name="password"
onChange={handleChange}
/>
</label>
<br />
<button type="submit">Submit</button>
</form>
);
};
export default FormComponent;
And you are almost right, with the little caveat that this is not React, it is React DOM (something independent from React), React and a compiler working together. Let's try to understand.
What's going on?
I know, the previous point was a little confusing, but it’s just about to make sense. Before, we need to understand how React is working behind the scenes.
React
React.createElement('button', { type: 'submit' }, 'Submit')
This, my friends, is plain React. Most of the code that you will see when writing React will transform into this. And a really good question to ask now would be: "What does this code do?" And the answer is: It mutates the VDOM (Virtual DOM).
What's the VDOM?
We mentioned before that mutating the DOM was expensive and extremely slow, and that React is the solution to this problem (one of many solutions). This is because the React method "createElement" doesn't directly manipulate the DOM; it manipulates the VDOM.
The VDOM is a tree-like data structure and conceptual representation of the real DOM that keeps track of our React components, like, for example, the button that we just created. It also keeps track of metadata that helps React to more efficiently mutate the UI.
The process that React follows is: each time something changes in the UI, for example, when we mount or unmount (mount refers to inserting the component into the VDOM) our button component, it’s compared to the last VDOM render with the current version, and updates the parts affected by our actions, in this case the div element and the button. It would look something like this:
This part that I just explained here is called reconciliation. If you want to know more, go and check React documentation.
What's React DOM?
React DOM is a library that is mostly used with React to render elements on the screen, giving us the tools and methods required to render our components (our React code). A method that we normally use is:
ReactDOM.createRoot(document.getElementById("root")!).render();
This method creates the so-called "mounting point," which is the basic element that will contain our entire application. In this case, and most often, our mounting point is a div with the id: "root".
What's a compiler?
Before some people jump to my neck, let me do a little clarification here. Explaining what a compiler is can be really complex, and in this case, it doesn't make sense. Not to mention that the definition of a compiler and a transpiler is blurry. So for the purpose of keeping this post more understandable, I'll keep it simple and won't dive into the details.
Most often, you will see React and React DOM used in this way:
const Component = () => (
<div>
//our code
</div>
)
This, my friend, is JSX. And we are able to write this understandable and beautiful code because of a compiler. A compiler is a program that translates code into some other code. It can be low-level code or just any other code. It doesn't even need to change the programming language, like in this case. We are transforming React JSX, which is just JavaScript with a special syntax, into normal JavaScript, giving us the possibility to combine a markup language like XML with JavaScript, hence the name: JSX (JavaScript XML).
There are many compilers that can transform JSX into JS, but the one that you probably are going to hear of is Babel.
What's the state and why should you care?
We mentioned before that React keeps track of the modifications that we do on the VDOM, but something that I didn't mention was that modifying the VDOM doesn't only mean that we are going to mount or unmount parts and React is going to notice that. React also keeps track of the mutations that happen to the state.
The state is a data structure where you can store almost anything (there are some limitations, but we are going to check them in the next chapter). React keeps track of this "store," and each time you modify it, it's going to notice and will reconcile (if you don't understand this term, go back to "What's the VDOM").
How do we manipulate the state?
To manipulate the state, we will use the React Hook useState (Hooks are just pieces of code that execute some logic that uses React, like a function-helper but with React code). Let's see:
import {useState} from 'react'
const Button = () => {
const [amount, setAmount] = useState(0)
const increaseAmount = () => {
setAmount(amount+1)
}
return <button onClick={increaseAmount}>This is our amount {amount}</button>
}
Here we are using the state to store a number that each time we click on our button will increase the amount.
The final piece for reconciliation
So far, we have seen that reconciliation happens when we alter the VDOM structure by mounting or unmounting, when we alter the state, and finally when we change the props of a component.
What's a prop?
React was created with code-splitting in mind, so in order to make our components more flexible, we can use props. Props are just values that we pass down from one component to another. Something just like this:
const Button = ({text, onClick}) => {
return <button onClick={onClick}>{text}</button>
}
export default Button
import { useState } from 'react'
import Button from './Button'
const Landing = () => {
const [counter, setCounter] = useState(0)
return (
<div>
<Button text="I'm a button" onClick={() => setCounter(counter+1)}/>
</div>
)
}
As you can see, we are moving the state up. This is known as lifting up the state, and also we customized our button.
Default props:
There are some props that come by default in React, they are really helpful
The key
const ImageGrid = ({images}) => {
return images.map((src) => <img key={src} src={src} />)
}
The key is normally used when you are doing a map to dynamically transform data into a bunch of elements, like in this case, where I'm rendering images by mapping over a list of them and I want to help React keep track of them, in case these images change. The key is always a unique identifier.
Children
This is used to pass a component inside another one. For example:
const Button = ({children, onClick}) => {
return <button onClick={onClick}>{children}</button>
}
export default Button
const Icon = () => {
return <svg />
}
export default Icon
import Button from './Button'
const Landing = () => {
return (
<div>
<Button>
<Icon/>
</Button>
</div>
)
}
If you change any prop, expect React to know it. The only exception to this is when you are passing a value by reference, but we’ll talk about it in the next chapter of the series. Follow me to don't loose it.
Before moving on...
If you are really enjoying this post and consider that my effort is worth promoting, take one minute to give me a hand buying me a coffee. You have no idea how helpful and motivating this is.
Why so much drama?
The name of this library is really descriptive, and you are just about to understand why. We now know that React code creates a visual representation of our UI, compares it between the different updates that we can do, and updates it as efficiently as it possibly can.
But let's be honest, isn't this a little bit overkill to just write some weird HTML? Yes, it is, but we are not just writing weird HTML; we are overcharging it with React, giving us a powerful interface to dynamically render elements on the screen and connecting them with our JS code, all in the same place.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Button and Message</title>
</head>
<body>
<button id="show-message-button">Show/Hide Message</button>
<div id="message-container"></div>
<script src="script.js"></script>
</body>
</html>
// Function to toggle the <p> tag with a message
function toggleMessage() {
const messageContainer = document.getElementById('message-container');
const message = document.getElementById('message');
if (message) {
// If the message exists, remove it
messageContainer.removeChild(message);
} else {
// If the message does not exist, create and mount it
const p = document.createElement('p');
p.id = 'message';
p.textContent = 'Hello, this is your message!';
messageContainer.appendChild(p);
}
}
// Add event listener to the button
const button = document.getElementById('show-message-button');
button.addEventListener('click', toggleMessage);
This is all the code that we need to render a button on the screen that will work like a switch: when it's on, it will render a message on the screen; when it's off, it will hide it. It's not that complex, but let's see how this can be done with React.
import { useState } from 'react'
const MessageSwitch = () => {
const [show, setShow] = useState(false)
return (
<>
<button onClick={() => setShow(!show)}>
{show ? 'Hide' : 'Show'} Message
</button>
{
show ? <p>Hello, this is your message!</p> : ''
}
</>
)
}
export default MessageSwitch
Yep, this is all the code that you need. That's why React is amazing. At this point, you should now understand what React is, what the state is, what the VDOM is, and what reconciliation is. You now have all the basics, so you are ready to start working with React.
How to start a project?
Probably you are asking yourself: "Now I have to install React DOM, React, and Babel to start the project?" Nope, there are scaffolding projects and frameworks that implement all these technologies for you.
Vite
If you are asking what Vite is, let's give them the opportunity to tell you:
Vite (French word for "quick", pronounced
/vit/
, like "veet") is a build tool that aims to provide a faster and leaner development experience for modern web projects
This build tool is incredibly fast and super easy to use. It will configure a React project with the options that you choose, avoiding the need to configure a compiler, a bundler, hot reloading, and many other things. To use Vite, run the command:
npm create vite@latest
Insert your project name, select your framework (React), and select either JavaScript + SWC or Typescript + SWC, go into the folder, and do npm i. You should have something like this (I chose Typescript + SWC):
Congratulations, now let's dive into the project:
index.html
We can see our mounting point and the React implementation. That's all the HTML that the client is going to render when they first enter the application. Later, the script tag will run and the page will be populated with our React code.
src/main.tsx
src/App.tsx
This is the default code that comes pre-implemented with Vite. Let's see it. To run the React application in development mode, we have to run:
npm run dev
That will start the project on port 5173 and it will display this:
Project Structure
I remember when I was a beginner, I hated that no one told me how to structure my files, and to be honest, it took me a while to figure out a way to do it properly. There are many ways to do it, and how you do it will depend on the kind of project that you are building, but I'll give you one that is good enough for most cases.
You'll add two new folders: pages and components. In components, you are going to store the code that you are going to reuse all across the application. Something like this:
Probably you'll want to reuse your input and button across your application. You probably noticed that I created the component: Input, and the Logic. That's because ideally, we don't want our JSX and our Logic together. Let me show you how to do this:
import { ChangeEvent, useState } from "react";
const regex = /[!@#$%^&*(),.?":{}|<>]/;
const Logic = () => {
const [value, setValue] = useState("");
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.currentTarget.value;
if (regex.test(value)) {
return;
}
setValue(value);
};
return { value, handleChange };
};
export default Logic;
In the Logic component of the button, we use all the things that are not directly part of the JSX. In this case, we have a function that handles the change in the input value and changes the state. We are also validating for special characters, not setting the values if it is the case.
import Logic from "./Logic";
const Input = () => {
const { handleChange, value } = Logic();
return <input value={value} onChange={handleChange}></input>;
};
export default Input;
And here we have our Input component with the special Logic that we wanted to implement. This is a pattern that we are going to repeat all over our application. And yes, this Logic component is some kind of custom-hook (Go back to "How we manipulate the state")
Finally, in the pages folder, we will have a recursive structure (recursive means that it repeats) with folders that will describe features of the application, having on them multiple components that refer to that page specifically. Let's see:
Coming to an end
If you have reached this point, it means that you now have the basics to start working with React. Go and keep practicing, creating something of your own and applying these concepts. In the next couple of days, I'm going to bring you chapter two, where I'll talk about more complex topics and advanced techniques.
You have doubts - Get in touch with me?
If you have any doubts or you need help pushing your project or knowledge one step forward, contact me. I'm starting a new project where you can talk with me for an hour, and I'll try to help you to make the progress you are looking for:
The end
If you really enjoyed this post, please share it, follow, and like it. Thanks for reading and I hope I'll see you again.
Credit
If you like the picture of the post consider checking the work of Lautaro his the owner.