By Medina Sagi
"In order to win a competition, you need to start out as fast as you can… and then slowly increase your pace."
In this post, we will share how and why we took a few months old React based web app and refactored it, completely rewriting it. We hope this could be useful for web developers looking for ideas on how to revamp their web apps.
We are prone to mistakes. Because of that, we often need to review, reorganize and revamp our work. In software engineering, this phase is called refactoring.
Refactoring is valuable. It allows us to keep our software in a healthy state. It is not a special task that would show up in Aone. Done well, it's a regular part of programming activity.
Whenever we start a new project or a new development sprint, we use the latest and greatest technology and software design schemes. However, within a short while, new tech and new design schemes emerge. The design of a core code-base makes a big difference in how easy it is to make changes. Enhancements are often applied on top of each other, in a way which made sense at the time of creation, but which later makes it increasingly harder to develop further changes, features or improvements.
Our team's first React project, developed in late 2018 as an alpha version, now (early 2019) requires new features, more maintenance, and better performance to answer the high demand.
We had to improve our core code-base.
"Embrace change" === "Re-write a five months old app"
The React project we will relate to is called HyperML.
HyperML is a continuous research platform designed with researchers and developers in mind: to help them run computation-heavy tasks (like Deep Learning training) on cloud GPUs with minimum overhead in a streamlined fashion, via a web interface.
HyperML was built in 2018 as the team's first React app, has that, The best way to start was to find the quickest, best-configured jumpstart, and, no less critical, with the mindset for deployment.
"We're ecstatic about both the developer experience and end-user performance, so we decided to share it with the community" - Next.js
That sounded perfect, and it is, but then, we didn't use it's SSR, we needed more control, more and more flexibility and honestly, we didn't feel very comfortable with this "black box." Creat-React-App is a good alternative but still doesn't resolve our problems, we had to find a way to rid ourselves from all the "extra" scripts and packages but still, save us time configuring Webpack for best results.
As it happened, Jetpack just hit Github trending (Jetpack is a thin wrapper around Webpack and can be extended with any of the Webpack configurations) - It fits us perfectly. It gave us the basic best configurations and a way to extend it as we like.
As we removed every Next.js related code and all of its dependencies, we had to find another way to handle routing — this time, we choose Router5 JS. While react-router is the community preferred routing package for React apps, Router5 seems more mature, more stable, and fits our "Separation of concern" design principle.
Router5 (with its React plugin) allowed us to separate routing from React and use core React functions to handle things like lazy loading. Here is how:
[Root page]
import React, { Suspense } from 'react';
import Loading from 'components/loading';
import TopBar from 'components/top_bar';
// Common
const NotFound = React.lazy(() => import('pages/404'));
//...
// HyperML
const HomePage = React.lazy(() => import('pages/hyperml_pages/home_page'));
const JobView = React.lazy(() => import('pages/hyperml_pages/job_view'));
//...
// AutoML
//...
class Root extends React.Component {
//...
getContent = () => {
switch (route) {
case 'home_page':
return <HomePage />;
case 'job':
return <JobView />;
//...
default:
return <NotFound />;
}
};
render() {
return (
<Layout className="layout">
<Layout.Header>
<TopBar />
</Layout.Header>
<Layout.Content>
<Suspense fallback={<Loading />}>
<div>{this.getContent()}</div>
</Suspense>
</Layout.Content>
<Layout.Footer>
Alibaba ©2019 Created by IMVL
</Layout.Footer>
</Layout>
);
}
}
//...
Choose what's right for you.
HyperML was the team first React App, but it wasn't the only one.
As we explore other possible ways to manage our apps states, we found that there isn't one right way, we needed the choose the right way for each app, either it's Redux, MobX or React's default state management.
One thing was clear though, as long as you don't need to use a 3rd party package - don't use one. React's default state management is excellent if you use it right, but since we are rewriting an "old" project that uses Redux as state management, we started to look for a way to keep it close to default as possible but still keep the "Redux way."
We found this Medium article (a new library that implements state management that is built on top of the new context API), and after we went through its smart and straightforward source code, we choose to "npm install react-waterfall --save."
We end up with a much clearer code that more manageable to maintain and, as an extra, we could let go of all the packages that we had to use with Redux.
[Jobs Store]
import { setData, mergeData } from './utils';
const initialState = {
jobs: {},
lastActivity: {},
};
function updateJob(state, callback, newJob) {
const { lastActivity } = state;
if (lastActivity[newJob.ID]) lastActivity[newJob.ID] = newJob;
return {
jobs: {
...state.jobs,
[newJob.ID]: newJob,
},
lastActivity,
};
}
function deleteJob(state, callback, jobID) {
const jobs = Object.assign({}, state.jobs);
const lastActivity = Object.assign({}, state.lastActivity);
delete jobs[jobID];
delete lastActivity[jobID];
return { jobs, lastActivity };
}
const actionsCreators = {
setLastActivity: setData('lastActivity'),
setJobs: setData('jobs'),
mergeJobs: mergeData('jobs'),
updateJob,
deleteJob,
};
export default { initialState, actionsCreators };
[Projects Store]
import { setData, mergeData, addData } from './utils';
const initialState = {
projects: {},
};
const actionsCreators = {
setProjects: setData('projects'),
mergeProjects: mergeData('projects'),
updateProject: addData('projects'),
};
export default { initialState, actionsCreators };
[Index Store]
import createStore from 'react-waterfall';
import mergeStores from './utils';
import jobs from './jobs';
import projects from './projects';
export const { Provider, connect, actions } = createStore(mergeStores([
jobs,
projects,
]));
[Connect to react]
//...
import { Provider as StoreProvider } from 'store';
import Root from 'pages';
const App = () => (
<StoreProvider>
<Root />
</StoreProvider>
);
[Use "action" anywhere]
//...
import { actions } from 'store';
actions.deleteJob(jobID);
[Connect to component]
//...
import { connect } from 'store';
const Activity = ({ lastActivity }) => (
<Card>
<JobsList jobs={lastActivity} />
</Card>
);
Activity.propTypes = {
lastActivity: PropTypes.arrayOf(PropTypes.shape()),
};
export default connect(({ lastActivity }) => ({ lastActivity }))(Activity);
Once we got control over our project dependencies, we could choose what to add more wisely. We planned this refactor to achieve specific goals; one of them is to improve our development progress.
It is significantly reducing the project dependencies that allowed us to add one important library that helps us with the most time-consuming part of our development progress - Design.
Writing and maintaining our CSS code cost us a remarkable amount of time, using Ant-Design improved not only HyperML's UI and gave our users better experience, it significantly shortened the time it took us to developed new features and gave us more time to write better code.
React v16.8: The One With Hooks.
On February 2019 React v16.8 came out introducing "Hooks."
"We don't recommend rewriting your existing applications to use Hooks overnight." they wrote on that day.
However, embracing change is in our culture. As we improved our current code, we started replacing class components to functions with Hooks, writing new components with Hooks and building our own Hooks.
Looking back, it helped us understand Hooks well, and by understand it well we manage to improve our coding time significantly, (Yes. using hooks helped us reduce the time it took us to write components) and write a better more readable and maintainable code. We couldn't achieve that if we didn't rewrite more than 50 components.
To improve your app performance, you may need to modify your server side code, however, if there are too many tasks on your server backlog you need to come up with solutions on the front-end. Here are two things that improved our app performance without the need to make changes on the server side:
Part of our app pages contains lists of jobs; those lists can grow large. Rendering an extensive list is a complex task for most browsers which cause impact not only on your app performance but on the user experience (facing a black white page until the browser finish to render the list).
To deal with this issue, we need to develop a paging mechanism; Usually, this is something that you would do on the server side, but since the main issue is with rendering the data to DOM and not fetching the data from the server we can develop it on the front-end.
To do that we choose to use react-infinite-scroller with this package, we can render only a small part of the list at first and load more every time the user scrolls down.
[Jobs list]
//...
import InfiniteScroll from 'react-infinite-scroller';
import { connect } from 'store';
import JobItem from './item';
const JobsList = ({ jobs }) => {
const [numOfJobsToShow, setNumOfJobsToShow] = useState(10);
const jobsInView = useMemo(() => jobs.slice(0, numOfJobsToShow), [jobs, numOfJobsToShow]);
return (
<InfiniteScroll
pageStart={0}
loadMore={() => setNumOfJobsToShow(showingJobs => showingJobs + 10)}
hasMore={numOfJobsToShow < jobs.length}
loader={<Spin />}
>
{jobsInView.map(job => <JobItem job={job} />)}
</InfiniteScroll>
);
};
JobsList.propTypes = {
jobs: PropTypes.arrayOf(PropTypes.shape()),
};
Another way to improve performance and user experience is to "help" the browser to fetch the data, especially if there is a slow internet connection.
We were able to do that by storing the app's data store in the browser's local storage. This way the user doesn't need to wait for the data (from the server) next time he visits the app.
There is one downside to this method; the performance impact of writing the data to the local storage every time there is a change. To overcome this, we use Web Workers for this task so that it runs in a background thread.
[Index Store]
import createStore from 'react-waterfall';
import mergeStores from './utils';
import jobs from './jobs';
import projects from './projects';
import { importLocalStore } from './localStore';
const localStorage = require('workerize-loader!./localStore')(); // ** Web Worker **
export const { Provider, connect, actions, subscribe } = createStore(mergeStores([
jobs,
projects,
]));
importLocalStore(actions);
subscribe((action, state) => localStorage.update(state));
[localStore.js]
// Runs on Web Worker
export function update(state) {
Object.keys(state).forEach(key => localStorage.setItem(key, state[key]));
}
// Runs on main thread
export function importLocalStore(actions) {
localStorage.getItem('jobs').then(jobs => actions.mergeJobs(jobs || {}));
localStorage.getItem('projects').then(projects => actions.mergeProjects(projects || {}));
}
We found that adding images to HyperML made the app more user-friendly and gave that little extra that makes work tools enjoyable. Undraw is a regularly updated Open-source collection of beautiful SVG images that you can use completely free and without attribution.
ESlint improves the team dev skills by forcing them to write better code. Consider using it on your projects.
Re-writing a few months old app may seem like a waste, but if you consider the time it will save us to maintaining the code, adding new features and developing new projects, it is clear that this reactor will not only be a proper use of our time but even a necessary task.
We started gradually, first rewriting some parts, while keeping other parts with minor changes. After approximately two weeks, we had a fully revamped working version of our web app. By now, we estimate that this refactor saved us hours of work for each new feature we developed since then, and there were plenty and still counting.
It was essential for us to share our experience and to encourage you to occasionally refactor and update your code.
We assume that our recommendation is sometimes hard to conduct, e.g. when the code base is huge and was not revamped for a long while. But maybe then it is even more important.
GCC vs. Clang/LLVM: An In-Depth Comparison of C/C++ Compilers
15 posts | 1 followers
FollowAlibaba Clouder - August 28, 2019
Alibaba Clouder - September 17, 2020
5251873121041033 - November 8, 2019
Hiteshjethva - October 31, 2019
Nick Patrocky - February 5, 2024
Alibaba Cloud Service Column - November 21, 2022
15 posts | 1 followers
FollowAlibaba Cloud (in partnership with Whale Cloud) helps telcos build an all-in-one telecommunication and digital lifestyle platform based on DingTalk.
Learn MoreBuild superapps and corresponding ecosystems on a full-stack platform
Learn MoreWeb App Service allows you to deploy, scale, adjust, and monitor applications in an easy, efficient, secure, and flexible manner.
Learn MoreExplore Web Hosting solutions that can power your personal website or empower your online business.
Learn MoreMore Posts by Alibaba Tech