Benedek Antók

What I've learned from recreating a PHP page in React

A four-panel comic in the style of the Drakeposting meme: Drake disapproves of PHP and endorses React

I've recently had the opportunity to recreate a formerly PHP-based page in React from the ground up. In this article, I will share some of the programming and UX concepts and takeaways I've picked up along the way.

This is a two-part article about web development. It gets technical, and it's intended mainly for people who are relatively new to 'Hooks-era' React development.

However, I will also be touching on a lot of framework-agnostic topics, so this might be an interesting read even if you don't use React. The second part, in particular, focuses on ideas that are worth considering when designing any product with a UI.

Table of contents


Structure

One of the most important things to consider when building a project is structure.

Component-based architecture

Long gone are the days where 10 000-line monstrosities of source code had any right to exist.

Today, modularisation is king.

In front-end development, we have components to that end.

Instead of treating your application like a monolith, you should break it down into smaller parts. In the case of a blog, you can do that like so (items in brackets represent arrays):

Components can be composed, meaning they can have a parent-child hierarchy.

In the above example, the Links to pages on the site are children of the Navigation element, whose parent is the Header.

If you notice a repeating pattern in your page's structure, you can probably turn it into a component and create instances of it with different properties.

In a blog, all the articles have the same basic structure and style, but different content. Their unique properties are the title, date of release, and content body.
They are a good candidate for a component.

React

React is a front-end library built around the idea of modularisation. It's what I'll be discussing in the first half of this post, and a broad and interesting subject in general.

For example, React uses its own internal model of the web page that it is displaying to increase its efficiency in updating the user interface that you see. It's also not limited to rendering web pages.

I won't go into the specifics of its inner workings, as many have already written about it. For now, let's just say that it is a cool technology, fit for the modern web.

One of the nice things about React is its documentation. This is a framework that requires a certain mindset, and as such it has resources like an article detailing how to think in React, and when introducing new concepts, it gives you helpful hints, like how to break up your app into components.

It is common practice to house your components in a folder called components inside your project's main source directory, with a flat structure. The source folder for my blog would look like this:

Notice how there is no logo component. This is intentional – the logo only appears in the header, and it can be described with only a few lines of code. There's no need to isolate it.

Global objects

While a clear hierarchy is generally a good approach to UX design, some parts of your code might be required in multiple different places.

For example, there is not yet a native JavaScript function for creating an array of numbers in a specific range (i.e., making an array containing each number from 1 to 10). Since I need this functionality, I created a sort of 'global' function for it.

export const arrayFromRange = ({ start = 0, end }) => {
const array = [];
for (let item = start; item <= end; item += 1) {
array.push(item);
}
return array;
};

I've put this in a file called utilities/functions.js, and now it can be pulled into any component that needs it.

Some people will instinctually wince at the very mention of global anything when it comes to code. Sure, global objects should be used sparingly, but they do have their place. Creating global constants can sometimes be a good idea too, as we will see in the next section.

Environment variables

My application also makes use of an API to fetch the raw data that it will use to fill the list of blog posts with content.

Normally, a single version of this API with a single URL would be enough. However, as new features are added, I want to have a stable version, as well as an internal development version running side by side.

For example, an API that lists articles on a blog could use an URL like this:

https://blog.antokben.hu/getPosts?page=4

However, when I'm working on the app, I might want to use a different version of the API that supports filtering by keywords, but isn't yet stable enough to be made public:

https://test.blog.antokben.hu/getPosts?page=4&keyword=design

With environment variables, I can determine whether my app is running in a development or a production environment, and use the appropriate endpoint.

I'm using the Node.js runtime environment. Thanks to that, I can check the automatically created NODE_ENV variable during build time, and use it to decide which URL to use for my API calls.

export const API_URL = `https://${
NODE.ENV === 'development' ? 'test' : ''
}
.blog.antokben.hu`
;

To make this information accessible for all of my components, I will put API_URL in a file called constants.js inside my 'utilities' folder.

State management

When building a user interface, you will likely need to use some kind of state management. All controls on a form (text fields, radio buttons, checkboxes) have a state. If you're building a blog with page navigation as an SPA, that needs state management too.

State management the React way

React components can have state, and for smaller projects, that can be enough. Writing the logic for these used to be a bit awkward and verbose, but since the advent of Hooks, (more on those later) that is no longer the case. However, as you're passing state from one component to another, up and down hierarchical levels, chaos starts to set in.

An important principle when developing in React is one-way data flow. This means that parent components can pass information to their children, but information can't flow the other way. While this is a great tool to avoid many bugs and to keep things running smoothly, it can create complications.

Let's go back to our structure diagram:

If I click on link for a page, how will the article list know that it has to show a different set of articles now?

The Pagination's parent component, Article list could pass a setter function to its child to call – but that feels a little clunky. This would get even more awkward if the element that sets the page (let's call it Page button) wasn't a direct child of Article list, but was instead even lower in the component hierarchy:

Now I would need to pass the setter function to Pagination, then, from there, to each Page button.

This is such a common antipattern that it even has a name. Prop drilling is when you pass a property to a component only to forward it to one of its children. It's something that will inadvertently come up when you're developing a more complex project – something you will probably want to avoid. So the question presents itself:

How do you manage a network of interconnected components without losing your mind?

Global store

It's become common practice in the front-end field to move state to a global 'store', a single source of truth. Every component has access to the store, and with the use of selectors, they can even access small slices of it efficiently.
Any state in your app that's relevant to more than one component instance should live in that store.

React doesn't have built-in support for global state management. However, libraries for this purpose exist, such as Redux and MobX.

While both accomplish the same goal, their approaches are rather different. I'll be focusing on Redux in this post.

In Redux, there is a certain 'etiquette' for handling state changes. You can't just burst into the store and alter it to your heart's content. No – if you want to change the state, you dispatch an action, and a little something called the reducer will handle the rest.

Don't be intimidated by the amount of jargon in that last sentence. I'm about to explain it all through an example.

Reducers, the etiquette for changing state

In Redux, a reducer is a pure function (more on that later), that takes a state, an action to be executed on that state, and outputs a new state.

The following example is a simple reducer that controls the state of a lamp (which is off by default).

You give it the current state and an action, and it returns the new state, which is determined by this action.

const reducer = (on=false, action) {
switch (action) {
case 'turn_on':
return true;
case 'turn_off':
return false;
case 'toggle':
return !on;
default:
return on;
}
}

You might be wondering: if every component has full access to the store anyway, then why don't they just update parts of it directly? If you want to turn on the lamp, just write on = true; and call it a day.

A reducer is a declarative concept, meaning it describes what you want to happen instead of how it should be done. This is in contrast with the imperative paradigm, (prevalent in languages like C). When you write code that's 'closer to the hardware', it's likely to turn out less flexible and harder to understand.

It might come as a surprise, but as an aspiring electrical engineer, I much prefer the declarative approach. Yes, using clever techniques like combining ternaries, multiple assignments, and post- and pre-increments, all on the same line, makes me feel cool, but that can get hard to decipher when I come back to the code months later. And if it was someone else's code, I would probably dread working on it.

That aside, I admit that using a reducer is overkill when you're dealing with something as simple as a Boolean value.

However, as your state gets more and more complex, it makes more and more sense to enforce a strict set of rules over how it can be manipulated.

Actions are easy to log, and as such, they can help you understand what happens inside your app over time.
Being able to instantly trace back what action causes an error when something goes wrong has saved me a lot of time during development.

Reducing network usage with caching

Remember the blog post API from a little earlier? There's an improvement we can make there, when switching back and forth between pages.

We have memory (state), and necessary bits of information (articles), some of which are already in the memory, and some of which need to be loaded.

Instead of making a request to the server every time a user navigates to a different page on the blog, we could remember each downloaded page, and serve it from memory the next time it's needed.

This is called caching – the same technique that's used in low-level hardware memory, i.e. RAM.

I decided to store the results from each request in an associative array, labelled by the query that was sent out to the server when requesting it.

const state = {
page: 2,
articles: {
&page=1: [article1, article2, ...],
&page=2: [article11, article12, ...],
&page=3: [article21, article22, ...]
}
}

Now, when the user navigates to a new page, I can first check if that page is already in the memory, and if it is, I can load it from there. If not, I'll send a request to the server and store it, in case it's needed later.

Reducing user workload Session Storage

What if you're not on the first page of my blog,

  1. you click a link,
  2. you get taken to another site,
  3. and then come back to my site?

If I don't do anything about it, my SPA will be reset to the first page again, and you might get disoriented.

There are several ways to fix it. On the one hand, I could use a Router to handle moving between pages in my app, updating the address bar (like /page/1/page/2) and knowing which page to request once you return to a certain URL.

On the other hand, I could use the browser's built-in persistent memory, with a plain JavaScript method called sessionStorage. Session storage allows my app to remember its state as long as the tab where it was visited stays opened.
(It also has a sibling called localStorage, that can remember things permanently.)

Using this approach, I will set a variable in session storage each time you click an internal link on my page, like so:

sessionStorage.setItem('lastVisitedPage', state.page);

The next time you return to my app, it will check if this variable has been set, and load the correct page:

const lastVisitedPage = sessionStorage.getItem('lastVisitedPage');
if (lastVisitedPage !== null) {
dispatch('set_page', lastVisitedPage);
}

Okay, now my app is more pleasant both for you and the back-end.

But something's still not right... When I request a new page, I have no idea when the API is going to send me the articles. This is an asynchronous request, and it does not play nice with my store!

Right now, my app runs in a single thread. If I play by the rules of pure function reducers, it's going to freeze starting from the moment of requesting a new page until the data actually arrives.

In that frame of time, it won't care about you trying to interact with the page. If you click on a link, it will just sit there, unresponsive, and you will think, 'What a lame website...'.

We can't have that.

Side effects

My problem stems from the fact that by requesting a new page, my end goal is modifying the state of my application. But I have no control over when the answer arrives.

Pure functions

Reducers are pure functions:

A side effect occurs when you change something in a function other than the value it returns. Telling my reducer not to worry about the request once it's started, but to do something with the response once it arrives, would be a side effect.

Now I have two alternatives to choose from:

This is not a good position to be in.
However, as it turns out, there's another way.

Sagas

Redux-Saga is a tool that can handle side effects, based on the actions dispatched to the store. If I fire an action, saying 'Get this page!', a Saga could pick that up, download the page, and fire another action that says 'I've got the page, now you can display it!'.

This way, the pure function principle isn't violated, and I still get a responsive app. That's better.

Sagas are not a simple topic to wrap your head around, so I won't discuss them much further in this post. They use generators, a special breed of functions, and they can handle multitasking. If this is the first time you hear about them, chances are, you're not really sure what they actually do, and I can't blame you.

I'll present an odd analogy, in case it's any help:

If the Redux store is the mob boss, then the Sagas are the thugs doing the dirty work.

For now, all that matters is that they can handle processes that takes an indefinite amount of time.

Hooks

While we're on the subject of side effects, it's worth discussing React Hooks.

There are two kinds of components in React: class components and functional components.

A class component is, as its name implied, a class – a child class of React.Component to be precise.

Consider a simple class component that outputs a paragraph with some text. It receives the text as a property (for example, 'Hello!'), stores it in its state, and changes that state to 'Hi!', once you click on it:

class SimpleComponent extends React.Component {
constructor(props) {
super(props);
this.state = { text: this.props.initialText };
}

setNewText() {
this.setState({ text: 'Hi!' });
}

render() {
return <p onClick={this.setNewText.bind(this)}>
{this.state.text}
</p>;
}
}

...

<SimpleComponent initialText="Hello!" />

This will output the following HTML:

<p>Hello!</p>

Functional components are a little less verbose, but they have no constructor (as they are just functions). This used to be a problem: originally, they could not hold state.

There is a good reason behind this, and it's related to the way web pages are rendered in React. As far as the React DOM is concerned, a component is just a building block of the page.

Calling the component and storing its result in the React tree is similar to taking a LEGO brick out of the box, and placing it somewhere. It shouldn't exhibit side effects. A brick shouldn't change its size if you put it back in the box and take it out again.

However, as we've seen earlier, it makes sense for components to retain their state. Enter hooks.

Hooks enable functional components to do a variety of things that they couldn't otherwise do, in a predictable way. That might sound vague, but it's really hard to draw a box around them. One common thing about hooks, though, is that every one of them has a name that starts with the word 'use'.

For state management, there's useState. With its help, SimpleComponent can now be written as a functional component:

const SimpleComponent = ({ initialText }) => {
const [text, setText] = useState(initialText);

return <p onClick={() => setText('Hi!')}>{text}</p>;
};

The useState hook:

Functional components use syntax that is cleaner than that of class components. As such, they are simpler to write, easier to read, and (thanks to Hooks) just as versatile.


Open-source packages

When you come across a problem, chances are, you're not the first one to do so. Someone else will have probably spent more time thinking about that problem than you ever will, and with a little luck, they will have posted their solution in the form of an open-source package. Should you reach for it?

Pros

On the one hand, it's nice to have a robust solution ready with minimal effort. Packages usually focus on a problem set, rather than a single problem. A declarative approach to searching open-source packages is a good idea. If your search criteria are just wide enough, you'll find tools with solutions for problems that you'll only encounter later during development.

Cons

On the other hand, pulling a huge library into your project, and then only using a single function from it, might be unwise. There external code may also turn out to be poorly written, or outright broken due to being outdated. If you're unlucky, you might even come across a seemingly innocent package, that contains malicious code. And by bundling more and more code with your app, its build size will keep on growing.

Settling on a solution

With these in mind, it's easier to decide between implementing something by hand and using someone else's solution. If you pick the second option, you'll need to choose from multiple competing implementations. Here are some questions I ask myself when trying to pick the right tool for the job:

If time is not an issue, you can also try implementing the solution by yourself, even after deciding to use an external package. This gives you a better understanding of the nature of the problem you're trying to solve, making it easier to see the strengths and weakness of existing tools.

Accessibility

Inclusive design is a big thing for me.

Writing code, I try to be as clear as possible when naming my variables and functions. When I'm chaining methods, I break them up into smaller functions to make my code more abstract. I keep comments to a minimum, as good code is self-documenting.

An accessible user interface is no different. Get the structure right, use semantic mark-up, and you're almost done.

Of course, this is easier said than done. Thoughtful design goes a long way, but you probably won't nail the semantics the first time.
Maybe you never will.

If you're using Bootstrap, you'll probably introduce some semantically redundant elements. If the app you're building is dynamic, you'll probably want to spice it up with ARIA tags to help assistive technologies make sense of it (more on that later). Affordances are also easy to get wrong (Ever create a link that doesn't look like a link? Yeah, me too).

In front end development, it's important to make using your app as effortless to use as possible. By embracing inclusive design, you're not just helping 'disabled' people; you're creating a better UX better for everyone.

Disability is widely misunderstood.

It's not necessarily a permanent feature: if my vision is impaired from staring at my computer screen all day, that's a disability. Could you tell, just by looking at me, that I have this disability? Probably not. Most disabilities are not immediately visible.

Most people are only familiar with the medical model of disability. This is defined as 'a physical, mental, cognitive, or developmental condition that impairs, interferes with, or limits a person's ability to engage in certain tasks or actions or participate in typical daily activities and interactions'.

As people experiencing them have explained, it can be more helpful to look at disability as a state that stems not from the person, but rather a debilitating environment. I will show you an example of this shortly. The bottom line is, accessibility benefits everyone.

The WCAG is a set of guidelines that explain how to make web content more accessible to people with disabilities. WAI-ARIA is a suite designed to make web content easier to use with assistive technologies.

This is a broad topic, so tackling it gradually (i.e. searching for 'accessible blog ARIA') is a good approach. If you want to get started with making your app more accessible, check out the WCAG quick reference first, and look at the points that apply to your app.

WAI-ARIA and the WCAG are collections of small recipes that make your app usable to everyone, regardless of their abilities.

Allow me to highlight some of these recipes that I found interesting:

Keyboard navigation. Try 'tabbing through' your page (press Tab ↹ repeatedly to go through each interactive element). For many people, this is the primary way to navigate the web. (I also prefer not to use a mouse, though only for convenience's sake.) If there is a huge navigation menu with a lot of links, it could take a while to get to the main content. Consider adding a 'Skip to main content' button if that's the case.

Your app will be displayed on many screens of different sizes. Buttons and links should be big enough to tap on mobile. Adding support for navigation by swiping is also worth considering. If people will use your app on their phone, it's a good idea to display the sizes of downloadable files. When using a huge widescreen monitor, you should limit the maximum width of lines to make reading sentences easier. If you're not careful, the sheer dimensions of a user's device can become the source of disability – this is what I was getting at when I was talking about debilitating environments.

Motion can also be debilitating, or at the very least, annoying. The human eye is naturally drawn towards motion – this is a fundamental survival mechanism. In the wild wild west of banner advertisements and popup-ridden websites, that can be a detriment to our ability to stay focused. Browsers and operating systems nowadays offer a 'reduced motion' mode, which disables some of these distractions, but in a web app, this is also up to us to implement.

Security

Technologically speaking, the web is more secure today than it was 10 years ago.

Still, there are good reasons to be cautious, both as a user and as a developer. (Maybe even paranoid if you're also in network security, like me.)

Keep an eye on what external data goes into your app (and what comes out of it).

Hackers can abuse your app by entering special characters into text fields and injecting malicious data into your database. You can try and protect the back-end from inside your app, but it's primarily the server's responsibility to inspect and disarm the data that is sent to it.

On the front-end, it's more important to sanitize the data that you receive from external (and in some cases, even internal) sources.

In a man-in-the-middle (MITM) attack, a hacker could pretend to be the API endpoint that sends you the blog posts for a given page, and it could inject some malicious code into it, that ends up infecting your reader's computer.
Not good.

You could try writing some code that painstakingly combs through all incoming data, looking for exploits, and disarming them, but nobody really does that. It's too big of a task. When it comes to security, it's better to forget about your pride and use a sanitization library.

Delights

With the serious topic of security out of the way, there's just one more area I'd like to address: the goodies that make your app a fun experience to use.

Disclaimer: You should only add these enhancements to your app once the core functionality works smoothly. It's entirely pointless to add nice graphics or animations if the UI is sluggish or if errors are frequent.

Let it breathe

The first and most overlooked way you can improve UX is by paying special attention to whitespace. If elements are too close together, people will have a harder time distinguishing between them. Be generous with paddings and margins. Move related elements close and add space between ones that don't belong together. Use em and rem units to specify sizes. Don't be afraid to use weird fractions, like 0.37. Look at your app in its entirety, notice where the spacing feels wrong, and experiment with it. It's like tending to your zen garden.

Make it snappy

Still on the topic of how the app 'feels', try interacting with it. See what parts of it feel 'slow'. Try to fix the slowness in code.
If a high latency request can't be avoided, you can use some visual tricks to make the app feel snappier.

While waiting for the content to load, you can display a spinner. In the case of blog posts, displaying 'skeletons' might be even better. They offer a preview of the structure of the page without the content – helping your reader get familiar with the layout. I was surprised to discover how little effort is needed to create a skeleton loader, and how much shorter the loading times seemed once it was added.

If using a spinner, progress bar, or skeleton doesn't make sense, you can also add animation to the element that triggers the delayed event. For example, if you have a 'Submit' button, add some flourish to it, like lighting it up in green for a moment, or adding an animated outline.

Go easy on the eyes

Adding dark mode to your app can also spark joy in your users, as more and more people are using the feature to reduce eye strain in their day-to-day browsing.

Animate

Animations help people understand how components and events are connected.

The Material design system has some great and easily digestible documentation, and it's worth checking out. Its section on Motion can be applied to any user interface, regardless of what design system it uses.

The name of the game here is function over form. Animations should be short and not too flashy. Their purpose is to provide context. Done well, you can even sprinkle a little self-expression into them (like making them bouncy), if the tone of your brand allows for that.

You have no way of controlling how fast your user's internet connection or computer is, but with tweaks like these, you can at least control the experience.

It almost feels like cheating!


Conclusion

If you've made it this far, cheers! Now you have a basic idea about many of the more obscure mechanisms present under the hood of modern web apps. We've also gone through some lesser-discussed facets of development in general.

If you're a developer, I hope you've learned something new from this post, or at least rediscovered some forgotten concepts that you had already come across. Feel free to send me an e-mail in case you have questions or observations.

Thank you for reading.