Framework Agnostic Guide to Modern JS Frameworks
Contents
Just to be fully transparent, I would still consider myself in the novice category when it comes to web development. At the time of this writing, I am not employed in tech and if were to seek a job right now, I would most certainly be hired on as a junior developer. That said, I have spent a considerable amount of time experimenting with the various available frameworks, and I think I have valuable perspective to offer. This article is directed to those who have learned a fair amount of the classic website structure using HTML, CSS, and JavaScript and just starting to ogle React or other such frameworks.
Modern Web Development
So you’ve learned about semantic HTML, you can whip up some CSS with a fair bit of ease, and you can manipulate the DOM at will with some fancy scripts. What’s next? Surely you’ve grown tired of copying and pasting an entire pages worth of html to a new document when you want to create a new page for your website, or making sure you’ve made the same change and all the different pages any time you update something in your layout. Or maybe you have a considerable amount of content on each page and you start to notice that loading times between pages is becoming quite noticeable. These are just some of the things that modern web development has taken great strides to solve.
A bit of a foreword, it is important to note that while I refer to React.js as a framework throughout this article, technically speaking, React.js is only a library. This is a bit of a point of contention within the community. The distinction is that React.js itself does not dictate how you structure your project and it does not impose strong opinions on how you implement its features. However, I refer to React as a framework throughout this article because it is very rare to see React used in a way that resembles a typical library. React is usually found embed within a framework (such as Next.js) or a template is provided by the bundler (such as Vite.js and React) which effectively acts as its own framework.
A Bit of History
Without going into extensive detail, for a long time, websites were mostly static. The structure you have been learning is very much what was used for businesses front facing sites. Fancy layouts were controlled by abusing the table
element and getting rounded corners on a single button required stitching together several images and some painstaking precision photoshop work. JavaScript let us create some fancy interactive animations and Macromedia Flash ran rampant throughout the internet. This was all fun and games of course, but any website that needed to actually accomplish anything and be taken seriously at all had to run some php in the back-end.
There was a whole phase of jQuery and Ajax that I completely missed, but from what I’ve heard, they filled many gaps in JavaScript at the time. Php was loved and hated, and still is to this day. WSIWYG editors gained a bit of popularity pumping out extremely bloated static websites. It was a bleak time and there did not seem to be any drastic improvements on the horizon.
Enter React.js. We gained the concept of only re-rendering the parts of the page that need it. We could finally come up with creative ways to handle state and create powerful web applications without relying so much on a back-end server.
I won’t claim that the modern way of building websites solve all of our problems and eliminates the back-end entirely. That’s absolutely not true, the new methods certainly solve many problems but consequently they have created plenty of new problems. And the back-end is just as, if not more important than ever before.
My personal experience is rooted in HTML and CSS from about 15 years ago when we thought that WSIWYG editors were going to eliminate the need for any manual coding. I abandoned web development only to return this past year. From my perspective, the contrast between the development flow back then vs is now is stark and much more pleasurable. People who stuck with it through the years seem to praise the peak of jQuery and Ajax, referring to them as the glory days of web development. I didn’t have that experience, so I can’t speak to the validity of those claims. But I can say that I very much appreciate the modern approach is it is today.
Tooling
If you have read any of other blog posts (or stalked my twitter account, creep!) you most likely know I have some strong opinions on operating systems and editors. Those are of little consequence in this discussion though. In my opinion, the two most powerful changes to the modern workflow is the advent of Node.js and Git. Node.js is far from perfect, I understand that, but among other things, it has vastly improved the way we approach building projects. As far as Git, version control software was inevitable, so I can’t pretend Git is mindbogglingly revolutionary, but its popularity means that I will likely never have to touch an ftp client ever again.
Most of these terms I’ll be using are likely derived from React.js, but many frameworks share the same terminology, though some may differ slightly.
Node.js
If you have only written raw HTML, CSS, and JavaScript, you may not know a whole lot about Node.js. The very short of it is, Node.js is a JavaScript runtime. That very basically means that you can run JavaScript in a program outside of a browser. There’s a whole thing about an event loops and whatnot, but I’m not getting into that here. The important thing for the context of this article is that Node.js allows use to make use of packages through something called the Node Package Manager (npm) and modules. With just those two things we can piece together quite an improvement to our work flow. I’ll talk on modules a bit later, but I’d like to introduce one particular type of package that has a huge impact.
Bundler
If you are not aware, the name “JavaScript” is a trademark owned by the Oracle Corporation. A bit of a tangent, but let me cook. So while we all use a language that we call JavaScript, we are abiding by the standards laid out for us in something called ECMAScript. Going into the details of ECMAScript could certainly fill its own article, so just know that ECMAScript extends beyond JavaScript, and its standards are released periodically adding new features to JavaScript. This updates are truly few and far between, and while we have been able to push a lot of really needed features into the mainstream in the past decade, there is a huge hurdle in getting the cutting edge features available to developers. That hurdle is browsers. There are too many of them, and too many people will sit on their browser for months or years without clicking that update button. Browser developers are slow to build in the newest features, and when they do, it takes ages to permeate out to the general public. As developers though, our saving grace is the fact that software like Node.js and its various packages can take advantage of those new features, churn your code a bit, and pump out a website that is compatible with the browsers in use today. These packages are called bundlers.
One of the early and more popular bundlers was a package called WebPack. WebPack was brilliant and solved a lot of problems. It allowed us to pull packages from npm, optimize our CSS, pre-process images, use ECMAScript modules, and more. After a tedious process of building your configuration, you are able to run a simple script, and your website is ready to launch, with minified code. Unused CSS classes are dropped, all your lengthy descriptive variable names are reduced to just a few characters, whitespace is eliminated, all is well. Except that Webpack aged, and it didn’t age terribly well. Faster and easier options were hitting the marketplace. So I will say, if you are following a tutorial that is using WebPack, and it isn’t explicitly walking you through the evolution to more modern solutions, you should probably look for an updated tutorial. It is worth understanding WebPack for the ability to work in older codebases. In 2024, you should probably just be using Vite.js. Maybe TurboPack if Vercel has successfully lured you into their lair.
Git and GitHub
The way we used to get websites to a server was to send files over FTP (File Transfer Protocol) which was fine for the time. It got the job done. But using a client to drag and drop files to the server felt archaic even back then. Git and GitHub give us the ability to manage versions, keep production and development code separate, and sync code to a server where someone else can automatically deploy it. And all that can be done with just a few commands in the command line. Brilliant.
Structure
Let’s get into the actual structure of a modern JavaScript Framework. We’ll start with a few core concepts.
Components
You may have run across the acronym DRY (Don’t Repeat Yourself) throughout your coding journey. While I disagree with this sentiment to an extent, the advent of components is an excellent execution of the DRY philosophy. Components allow you to write a bit of markup, attach some script to it, maybe if attach some CSS to it, and then use that component in several places throughout your code. The various frameworks have different structures for components, and different ways they handle props and state (which we will discuss later on). But the core function of a component is to create something once and use it several times.
Props
The way I think of props is that props are to components as arguments are to functions. They are simply “things” (whether that is a string, number, or object) that you can send to a component to be used inside the component. Props are one of the primary reasons components are able to be reused. Anything that we want in the component that may change depending on where we call the component, we call a prop in its place. Props are commonly passed into a component using syntax similar to an HTML attribute.
JSX
If you are familiar with template strings in JavaScript, JSX should come pretty naturally to you. It is essentially HTML that is embed in JavaScript that allows JavaScript to be injected in any point.
// Template String
const templateString = `<p>Hello ${name}</p>`
// JSX
const jsx = (
<p>Hello {name}</p>
)
In this simplified example, the differences are very small, but in both cases the portion between the curly brackets can be any valid JavaScript that returns a string. While many HTML attributes may be used in JSX there are some attributes that clash with reserved JavaScript keywords such as class
, so in JSX you must use an alternative dictated by the framework you choose (e.g. className
in React).
While it is possible and perhaps even desirable to assign JSX directly to const
as in the example above, it is far more common to return JSX from a function:
const FunctionComponent = () => {
return (
<p> Hello World!</p>
)
}
This is where props can come in to play. We can pass data into that JSX from outside the function component:
const FunctionComponent = ({name}) => {
return (
<p>Hello {name}</p>
)
}
// when the component is called you
// may pass the prop in like so:
(
<FunctionComponent name="Logno" />
)
The syntax for handling props differs between frameworks, but they all function broadly the same.
Layout
Layouts in most frameworks are generally just a semantic distinction from a function component. This is where you include all the markup that makes up the bulk of your website. Here you will rope in your Header
component and Footer
component. This will also be where you focus on how your website will block out on different medias. So all the div
’s using grid
or flexbox
will be handled here.
A simple layout may look something like this:
<>
<Header />
<body>
<aside>
<InfoBox />
</aside>
<div className="content">
{children}
</div>
</body>
<Footer />
</>
(The <></>
is the result of a rule built into the framework that states that all functions must return a single parent element. So if you wish to pass sibling elements to the output, you must wrap them in these dummy element brackets.)
When you go to create individual pages you will call the Layout component like so:
<Layout>
// ...page specific jsx or components here
</Layout>
The {children}
will be replaced by whatever JSX you have between <Layout>
and </Layout>
.
Again, the syntax may differ based on the framework. Astro, for example, uses <slot />
instead of {children}
.
State
So far, what we have is enough to eliminate a ton of repetition in markup. But that’s not really what is truly exciting about front end frameworks. We want to have content on a page that can change and we don’t want to reload the whole page just to see that happen. Yes, simple DOM manipulation can do that, and for simple applications, its not too difficult to implement. But you may just need to pass around a significant amount of data, and it can be rather cumbersome to write all that DOM action and storage in vanilla JavaScript. That’s where state comes into play. In the following examples, I will use React syntax. Other frameworks of done fantastic jobs trimming down the syntax making it quite a bit simpler to handle state, but I believe if you can wrap your head around state in React, it will be easier to understand elsewhere.
See the classic counter example below:
const Counter = () => {
const [count, setCount] = useState(0);
return (
<button onClick={()=>setCount(count + 1)}>{count}</button>
)
}
A lot is happening here. First, note that we used const
rather than let
. This is good practice. let
should only be used when absolutely needed, all other cases should use const
. Next, the array syntax on the left the declarative =
is called deconstructing. The useState()
function supplied by React returns two items, so when we call it, we want to assign those two items appropriate names. The first item should just be the name of the variable we want to set. The second item is a function that sets a new value to the variable when its called. This may seem unintuitive, why not just use let
then we can change it directly as we wish. State in React is explicitly immutable. By using the useState
function, we are allowing React to take control of that variable and manipulate the DOM only as needed based on the changes to that variable. If you simply set a value to a variable using let
, you can then change that variable at will, and the user would never know unless you feed that to the DOM somehow. By using useState
, we are ensuring that everywhere count
is called, we are erasing the old count
from existence, and replacing it with a brand new value.
Now with a few basic components explained, lets piece together a basic project.
File Structure
File structure may be dictated by the framework you choose to use. However, there seems to be a common structure that looks something like this:
.
|-node_modules/
|-public/
|-src/
| |-main.jsx
| |-App.jsx
| |-App.css
|-package.json
|-README.md
|-index.html
|-vite.config.js
|-.git/
|-.gitignore
I would say that this is the bare minimum for a React project.
The node_modules
directory contains any packages that are listed in your package.json
. It is very important to note that the node_modules
directory should only be stored locally, that is, the node_modules
should be included in your .gitignore
file. As your project grows, the node_modules
can begin to swell significantly and it is a cardinal sin to push all that data to the remote server.
The public
directory is a special directory used by the bundler to expose unprocessed files to the built app. In other words, putting an image file in the public
directory is equivalent to placing an image file in the root directory of a traditional HTML website structure. To access an image in the public
directory from a file in the root of the src
directory, you treat relative path names as if they are in the same directory. For example, say you have an image in the public
directory called hero.png
and you wish to include with the JSX of your App.jsx
. You would do so like this:
<img src="/hero.png">
The src
directory is where all your code lives. Depending on the framework you are using, the way the code will be injected may differ. Some bundlers or frameworks may require you to have a plain index.html
file in the root directory of the project that sources your main.jsx
file. Some frameworks, Next.js for example, will build the output entirely from an app
directory given that some specific files are present.
Within the src
directory are some key files. Starting with the main.jsx
, if you have any experience with other C like languages, there is always a main
function. The main.jsx
file mirrors that functionality as this file acts as a funnel for all of your code. Your index.html
sources this file, and this file reaches out to all other resources as needed. This file is typically very small, it may only be 5 or 6 lines of code, as it only needs to reference a DOM entry point, and then inject your app code into that point. This will also be where you import your top level framework modules. A basic main.jsx
file may look something like this:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
Notice from here, App
is being imported from ./App.jsx
. This file is where the meat of your project will be. This is, for all intents and purposes, your index.html
. The JSX written here will be the output when your site is visited at the root domain. A simple App.jsx
may look something like this:
import Layout from './layout.jsx'
import './App.css'
function App() {
return (
<>
<h1>This is my React test</h1>
<Layout>
<h2>This is some stuff inside my layout</h2>
</Layout>
</>
)
}
export default App
This app would require a layout.jsx
to be present in the src
directory as well.
The package.json
is a file that contains the names and versions of the packages you will be using in your project. You may add items to this manually and the packages will be installed when you run npm install
within your project root. Alternatively, if you install packages via the command line (npm install <package_name>@<version>
), the appropriate line will be added to this document as well as the package being installed. This file allows you to install all the needed packages with a single command. As mentioned before, we do not want to sync our node_modules
directory to the remote repository, so if you or someone else clones the remote repository, they may install all the needed packages and their respective versions.
The README.md
should be self explanatory. This is where you should explain your project and any build instructions required to run the project.
The index.html
, depending on you structure build your project, may only be some simple meta data, and then a div
with an id
that will act as the injection point for your app.