21 Oct Defining our 2023 architecture for frontend React applications – part #2
Welcome back to the second part of the blog post series!
In the first part of this blog post series
Here is the Agenda for this second part.
This is hidden by a class in custom class but used for the links generated dynamically do not remove please
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris tempus nisl vitae magna pulvinar laoreet. Nullam erat ipsum, mattis nec mollis ac, accumsan a enim. Nunc at euismod arcu. Aliquam ullamcorper eros justo, vel mollis neque facilisis vel.
So let’s keep going and continue with the one that we left incomplete.
4). Small and slim components/hooks/classes/functions and the Single Responsibility Principle
It really seems like there is a trend to stick a `bunch of app logic in the views` (components) these days with the adoption of `Hooks` and `functional components`. It seems really simple and trivial at first, but as the app grows or you need to refactor, it can get very messy and difficult to maintain.
Splitting into small chunks of a big component is a good practice, and being aware whenever a component is doing a lot of stuff, responsibilities such as `retrieving data` from the backend, iterating through the elements, and printing all details for individual elements, so in those cases, we need to identify if we are violating any software development principle(Single Responsibility one).
What if we enforce an agreement like this?
Not more than 200 lines of code in a single file. If it's growing more than that refactor it into smaller components. Also, follow one of the design patterns included in the next sections. It keeps the view (which is mostly verbose JSX) and your logic (JS with hooks) separate. It also makes your code easier to reuse
For instance: in the project we are analyzing, in one of the big components we are doing quite a lot and it would be doing more shortly (maintainability would be more difficult) for example, there is a requirement to send email notifications after a form is successfully submitted, and for that, looking into similar code in the same code base, most likely we might be directly accessing data on the Global state(Redux in our case) and then triggering new actions, but thanks to some code reviews and discussions we were able to move all that logic into a custom hook and then we just needed to import that custom hook and trigger it in the right place, so no more than 3 lines would be added to the existing big component.
Now, we can move forward and look into the following aspect.
5. State management
Sooner than later we would need to manage a global state and interact between children, parent components, and things like that.
5.1 What is state management?
State management is essentially a way to facilitate communication and sharing of data across components. It creates a tangible data structure to represent the state of your app that you can read from and write to. That way, you can see otherwise invisible states while you’re working with them.
Most libraries, such as React, Angular, etc. are built with a way for components to internally manage their state without the need for an external library or tool. It does well for applications with few components, but as the application grows bigger, managing states shared across components becomes a chore.
Evaluating our current React Redux architecture
For this project some months ago, some engineers decided to keep using React Redux, because of some analysis and because an associated project was already using React Redux, but in recent years, more alternatives appeared in the market and as part of this architectural analysis we decided to review this aspect of the global state management and the next sections would be about that.
5.2 Why are we considering Recoil.js for the global state management?
Recoil.js lets you create a data-flow graph that flows from atoms (shared state) through selectors (pure functions) and down into your React components. Atoms are units of the state that components can subscribe to. Selectors transform this state either synchronously or asynchronously.
5.3 Should you deprecate your current implementation of React Redux?
To answer that question, we can start watching this video, and see how dead simple it is to use Recoil.js
In this video Dave explains very well the usage of Recoil.js and shows how simple it is, so at the first glance, you might say, of course, let’s start using this new super cool dependency, as you can start using the `useState` hook, and once there is a need to start using that from somewhere else, you just basically need to replace it with `useRecoilState/useRecoilValue` and that's it, as simple as that, you can convert a local state into a sharable and syncable element in your state.
A quick way to start using Recoil is by using `useState` and `useEffect` just as we do for a few components currently, and at the beginning keep everything locally, doing standard React component props, and once we realized the first sight of `props drilling` or we had the need to access that data from a distant sibling or child component then it would be a matter of changing that to behave in the way that `useRecoilState` works and by doing that no other changes would be needed.
5.3.1. Let’s take into consideration technical debt
Let’s say we decided to deprecate our current Redux implementation, so once we install the `Recoil` package as a new dependency and the `root`, we would have an increment in our `tech debt` because of the usage of the existing `Redux` dependency in other places of the app, so that means, maintaining two dependencies that do pretty much the same and trying to migrate, refactor and deprecate Redux seems like plenty of work ahead without any visible gains for our managers, client or product owners.
In the current project for example, there are different scenarios and ways we currently use React Redux that will make the transition and refactor a little bit slower at the moment, and some of those edge case scenarios and usage look like this:
And if the team tries to migrate that into `Recoil` specifics, it would need some additional effort, so there is a chance that we would end up using/accessing both states temporarily and then we won’t have just one way to implement things(more inconsistencies and tech debt would be caused)
It is also important to mention, that the current functionality it is part of an MVP(Minimum Viable Product which I’m going to be talking about the problem with that mindset in some cases..) and planning this deprecation would involve a `medium-high` effort, so instead of doing it, we can work on other high-leverage refactors and improvements that would cause a higher impact on the project.
5.4 React Redux
Most of us know React Redux very well and the setup on the current project is working just fine, we've used it in an associated project, and we all know the power, pros, and cons of `Redux`, and with the creation of hooks the usage of global data is very simple these days
You might find this article very informative and relevant as of 2022
So you can see how `React hooks` with Redux can be utilized in more recent React versions(16, 17, and 18 at the moment).
Is Redux here to stay?
This question can have different answers depending on the use case. Redux still serves the purpose it was made for and is one of the most reliable state management libraries for large-scale applications.
The consensus among our team as of October 2022, was to keep using `Redux` for now and review in the future the usage of Recoil and how the transition would look like...
In addition to the most traditional and simpler way of passing properties to components and Redux - Recoil, there is a possibility to use `React Context` as a global state(More in dept analysis is included in the Design patterns section), and we are also starting to use `React Query` to help in this aspect(more information and details regarding React Query can be found in the section called `HTTP requests`)
6. Design patterns
These are typical solutions to common problems, in software design. Each pattern is like a blueprint that you can customize to solve a particular design problem in your code
React Design Patterns are core enablers of React library that enable React Developers to create complex React apps in an easily maintainable manner, which are also flexible, deliver better results, and drive better performance.
After the introduction of Hooks in `React 16.8`, things have changed a lot, as described by Dan in an update to the same blog
Update from 2019: I wrote this article a long time ago and my views have since evolved. In particular, I don’t suggest splitting your components like this anymore. If you find it natural in your codebase, this pattern can be handy. But I’ve seen it enforced without any necessity and with almost dogmatic fervor far too many times. The main reason I found it useful was that it let me separate complex stateful logic from other aspects of the component. Hooks let me do the same thing without an arbitrary division.
6.1 React Hooks
Better functional components
Since the announcement of Hooks, the React community has been very quick to adopt it. Likewise, React Redux has also added Hooks to its existing API. With a better developer experience and performance boosts, this API has brought some great improvements to codebases more inclined toward Hooks.
So the future is functional programming without using React classes and you can do the same with them.
6.1.1 Why React hooks?
When the React team brought to us these new hooks, it was with the intention of allowing us to use functional components, not just for static rendering, but to allow us to make more with them and be able to manage both state and lifecycle events.
A big motivation for that is because javascript functions are incredibly simple, so it seemed a shame to only be able to use them for read-only static rendering mostly, one of the benefits of React hooks design, is that they are easy to use, don't clutter your code with complex usage patterns, that's the good news.
The bad news is as we try to solve more complex problems, we end up using more and more hooks in our components, and we get back to complex, hard-to-understand, hard-to-reason functional components quickly.
The good news is that part of the React hook's design is that they easily compose itself into a custom hook.
The main difference before and after the introduction of React Hooks
And you don’t need to worry about `this` anymore and the usage of hooks does it without unnecessary nesting in your components tree, being more specific, hooks are just Javascript functions that allow developers to use state and cycle methods inside react functionality components, so you can move without even using Javascript classes, and we will be using the most common hook in React:
- useEffect
- useRef
- useState
The `useEffect` that everyone knows for sure, and here is a quick comparison
In this quick example
Based on the empty array, it means that this function runs only once as it is not going to change the array, and if you want to render a second time, you will need to include in the array, the elements you want to listen for, in other words, the value(s) that the component is dependent on.
6.1.2 React hooks basic usage
React Hooks can be used only within functional components and only from the top level, if we are going to use use them, we can install the following plugin so we can make sure the right usage of hooks
and it would help you understand when `React` hooks do not meet the basic rule.
By supercharging `functional components` with the ability to track internal state, access component lifecycle, and other class-related features, the Hooks pattern solves the class-related problems mentioned above. As pure JavaScript functions, React functional components are composable and eliminate the hassle of working with this keyword.
6.1.3 Classes vs functional components comparison
React classes syntax
With React functional components and hooks
6.1.4 Custom hooks examples
We can create custom hooks following the same conventions, for example, for fetching data(in this example I'm using Axios as a dependency for HTTP requests).
src/hooks/useBlogEntryManager.js
And here is the way we can use it in our components.
6.2 React Context API
6.2.1 What is the Context API?
Let's take a look at the official documentation:
In a typical React application, data is passed top-down (parent to child) via props, but such usage can be cumbersome for certain types of props (e.g. locale preference, UI theme, user Data, etc.) that are required by many components within an application. Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree.
The two major drawbacks of relying heavily on the Context API:
6.2.2 Before You Use Context
Context is primarily used when some data needs to be accessible by many components at different nesting levels. Apply it sparingly because it makes component reuse more difficult and if you are using too much of that(you rely heavily on the Context API for basic state management), the Context API uses a comparison algorithm that compares the value of its current state to any update it receives, and whenever a change occurs, the Context API broadcasts this change to every component consuming its provider, which in turn results in a re-render of these components.
6.2.3 If you only want to avoid passing down some props
If you only want to avoid passing some props through many levels, the `component composition` is often a simpler solution than context(For more information about component composition please review the section called `Component composition`)
The `React Context API` is used in implementing features such as the currently authenticated user, theme, or preferred language where global data is shared across a tree of components.
You are often advised to rely less on `Context` or other libraries for local state management, especially if it’s in a bid to avoid `prop drilling` and component composition is easily your best bet(please take a look at the section called `component composition` for more information).
So in other words, let's start very simple following the traditional properties passing down and once we notice that we need to pass more props to the nested children and that some of them won't be using them in any form, then we can evaluate the composition pattern and finally jump into the Context API.
6.2.4 Using simple props vs Context
Here is a good example and a comparison of the usage of Context in the children's components vs the most regular and traditional way of passing props to the right child, the usage of Context adds complexity when in fact can be avoided initially unless this component in the future starts growing and then we end up with the `props drilling` problem(For more information about prop drilling please take a look into that section)
6.2.5 Some additional details we can take into consideration when applying the React Context.
To avoid importing contexts from weird classes or components it is much better to organize them in a specific, and in our case would be the `src/sharedContexts/` folder and you can import your contexts from that location, for example:
Allowing colors customization example.
The second example using a real use case for Context would be for themes and white labeling for your application, so let’s combine it with `hooks` and see how the final organization of files would look like.
We would start by placing our context within the existing `src/sharedContext` folder:
src/sharedContext/ThemeContext.js
Hooks are placed within the `src/hooks` folder
src/hooks/useTheme.js
And then you can use this in your components when you need to access data from that specific Context
src/components/Layout.js
6.3 Component composition
If you only want to avoid passing some props through many levels, the component composition is often a simpler solution than React Context.
When we build React applications, we do so by building multiple reusable components that can be viewed almost like independent Lego blocks. Each Lego block (component) is then considered to be one piece of our final interface — which, when assembled or composed together, form the complete interface of our application.
It is this process of assembling components as Lego blocks that are known as component composition.
6.3.1 Container components
Quick example
6.3.2 Parent component
6.3.3 Specialized components
6.3.4 More examples of the parent and container components composition style
Container style
6.3.5 Parent composition style
So, in general, those are the recommended patterns when it is preferable to use component composition to reduce complexity, Do you use any other styles? please let me know, as we all want to learn new ways of component composition.
7. Automated testing
In my personal opinion, I think this aspect of the architecture is quite important and it deserves its own blog post, but in this quick and short section, I would briefly talk about Unit testing and in a separate blog entry that is in progress already I will be talking about e2e(end-to-end)/integration/system tests and by having a lot of influence from the Rails community(Ruby background), in that guide I’ll be looking into the options(Capybara, Cucumber, Gherkin, Cypress, Continuous integration, mocking or not mocking data, etc) and analyzing some of the options you can consider to automate tests for your Single Page Applications, and I already titled that draft as:
How to architect integration or End-to-end(E2E) or system tests for frontend Single Page Applications(React, Vue, Angular)
And once I’m done with that, I’ll come back here and update the reference, but I wanted to mention automated testing since the beginning as that is an important aspect of a clear architecture and in many cases that would help you to have a better definition of the application, it would also serve as a documentation and to help you build with confidence and you know all the benefits that we get from having a good test suite coverage.
7.1. Unit testing
Unit tests are beneficial in ensuring the code works as intended - it is the first step towards being confident about the logic.
Unit tests should always answer two questions:
- 1. What is the input?
- What is the expected output?
Tests, like your code, should be easy to read and understand.
At the end of the day, it's not important to decide on the kind of unit test styling you are following but to include automated tests whenever you see a good value in it.
One accurate guide or principle we can follow when in doubt about adding or not unit tests to our Javascript would be, to ask for a fresh pair of eyes from an engineer to look for a few seconds into that `class/method/function` and tell you what is the input and output and if that is not easy and simple to get the right answer then that means this is a good candidate for adding unit tests and covering the crucial scenarios, also it would serve to guide where to put some logic as the test would become very complex to be prepared and exercised if the function/method is doing too much
One specific example that could help us validate that, would be one hook that we recently created and that looks like this in our project:
We can see a lot of dependencies here, and at the first glance, it is not completely clear what the input and output are.
In fact, once we get started with the unit test and exercise this functionality we would realize that it is not easy to exercise, and then we might need to decide on refactoring or moving some logic into a separate file or class.
So, that’s it for now on the unit testing, a very quick and short lesson, isn’t it?.
Thank you for reading and if you are interested on:
How to architect integration or End-to-end(E2E) or system tests for frontend Single Page Applications(React, Vue, Angular)
So, this is the end of the second part of this entry and the next piece, if you are interested, would be about:
- CSS and styling
- How to manage your HTTP requests
- How to manage Forms and validations
- Decoration of data and more aspects
- And finally, we would put all the aspects together!!!
Thanks for reading and see you folks in the next piece.
H.
No Comments