Frontend Migration: A Journey Through Time and Space
Over the course of several months, our team has worked through a sizable migration of frontend code from one bounded context (BC) to another.
“Why would you do that to yourselves!?", you may be asking. And the answer is pretty simple: our team was in the process of extracting part of a legacy monolithic code base into a separate BC, including both the backend and frontend services. A driving motivator for this effort was that the legacy monolith had become cumbersome to maintain, especially as many people with historical knowledge of its systems had left the company. While moving over this code verbatim was an option and one which might have been easier in the short term, it also would have meant installing some large dependencies that we were actively looking to remove, namely Redux and Enzyme.
While some of that migration was surprisingly and pleasantly smooth, other parts of it were… not so smooth. ð
The word “migration” is enough to make the hair stand up on most developers’ necks; whether it be with data, framework versions, language versions, or something else, migrations are invariably messy, even at their best. In my experience, part of the “mess” in migrations has always included one thing: inconsistency with the input. For our team, the input was frontend code which had last been touched anywhere from several weeks ago to several years ago.
So now without further ado, I invite you to follow along as I regale you with the first part of our journey’s tale.
Table of Contents
- Plotting Our Course
- The Danger On The Horizon
- Testing the Waters
- Staying the Course
- Home Stretch
- Out with the Old, In with the New!
- Shore Leave: Final Thoughts
- But wait, there’s more!
Plotting Our Course
To make an otherwise daunting task feel achieveable, we started by agreeing on some high-level goals:
- whenever possible, class components should be refactored to functional components
- in our component tests, replace Enzyme with Testing Library
- the primary reasoning is pretty well articulated in the Testing Library’s own docs
- for state management, replace Redux with the Hooks API
- change styling strategy from CSS modules to styled-components
Additionally, we agreed that we should break up the work into bite-sized pieces so that they were easy to review.
This point was especially important to our team; as we primarily practice mob programming, it was essential that the migration work proceeded in such a way that it did not disrupt the main workstreams of our two mobs.
In practice, this meant that we’d aim for pull requests to encapsulate work on a singular component, including its tests. First, we started with components at the leaves of each branch of the component tree and worked upwards through the rest (i.e. static components first and only starting stateful components once their children had been migrated). For other tasks, such as moving over constants, utility functions, etc., we deferred to our main directive: keep pull requests small and easy to review.
The Danger On The Horizon
If you’ve spent more than a year or two in the industry, then you’re almost certainly familiar with some of the challenges of working on legacy application code. As evidenced by the git blame, ownership will likely have changed hands at least a couple of times, maybe even over the course of 6 years or more.
Depending on how long ago pieces of the application were created, you might even see a variety of patterns and paradigms, shifting with major releases of libraries, frameworks, languages, trends, and so on. And at least once, you will almost certainly think to yourself, “Why is this here? Can we remove it?” Hell, you might experience all of that just looking at one file!
In a nutshell, you can expect to find a lot of variables to juggle. (pun not intended ð)
Before we get our feet wet, so to speak, let’s look at one example of some existing code that needed to be migrated: a class component.
DISCLAIMER: These examples are, of course, not actual production code, though I suspect that will be evident by all the fun I had with naming. ðŽ
OldClassComponent
index.jsx
styles.css
test.js
As you can imagine, there was a lot of variance among the code being migrated (some of which hadn’t been touched in ~4+ years) but I tried to capture the most common aspects of those class components in this example.
So now with that example laid out, we can depart on our refactor journey.
Testing the Waters
As the punny title suggests, our journey begins with changing the existing tests. Since we strive to follow good TDD practices at Pluralsight, it feels like a natural place to start.
1 - Tests: Change imports
The first order of business is to change a bunch of these imports, many of which we will no longer need.
test.js
diff
import React from 'react';
- import { mount } from 'enzyme'
+ import { cleanup, render } from '@testing-library/react';
- import { Provider } from 'react-redux'
- import { createStore, applyMiddleware } from 'redux'
- import thunk from 'redux-thunk'
+ import { rest } from 'msw';
+ import { setupServer } from 'msw/node';
- import JustShowTheStuff from 'example/components/JustShowTheStuff';
- import ThingsToDoArea from 'example/components/ThingsToDoArea';
- import reducers from 'example/store';
- import createMockServer from 'example/tests/mock-data/mockServer';
- import OldClassComponent from '.';
+ import { NewClasslikeComponent, Provider } from 'example/components';
Keeping in mind our high-level goals, removing Redux and the Redux-adjacent imports and replacing Enzyme with Testing Library should be no surprise.
One less-straightforward change is the replacement the homegrown mockServer
with imports from msw
. For us, both the homegrown mockServer
and msw
solved the need for mocking server requests made from the frontend code in our tests. While there is certainly an argument for extracting all of our mock server setup into a centralized place, we have consciously decided to be more explicit in our approach to that setup by having any mock endpoints live within each test file itself.
I’m not going to go into the particulars of using msw
in this post but for your convenience, here are the doc pages for these two APIs we primarily use from msw
:
2 - Tests: Refactor mock server and other shared setup
With our imports changed, let’s now set up our mock server and refactor the other parts of the shared test setup.
test.js
diff
- describe('OldClassComponent component', () => {
+ describe('NewClasslikeComponent component', () => {
- let wrapper = null;
- let mockServer = null;
+ const userHandle = "1959b2d6-e209-4a5e-a2ca-d4a0fb5cac68";
- const store = createStore(reducers, applyMiddleware(thunk));
- mockServer = createMockServer(store);
- mockServer.setupMock();
+ const api = setupServer(
+ rest.get(`/api/stuff/${userHandle}`, (req, res, ctx) => {
+ ...
+ }),
+ rest.get(`/api/things-to-do/${userHandle}`, (req, res, ctx) => {
+ ...
+ })
+ );
+
+ beforeAll(() => {
+ api.listen({
+ onUnhandledRequest: ({ method, url }) => {
+ // NOTE: this method is here to make tests quieter
+ }
+ });
+ });
- wrapper = mount(
- <Provider store={store}>
- <OldClassComponent />
- </Provider>
- );
+ let wrapper = null;
+ const baseState = {
+ user: {
+ handle: userHandle
+ }
+ };
+
+ beforeEach(() => {
+ wrapper = render(
+ <Provider initialState={baseState}>
+ <NewClasslikeComponent />
+ </Provider>
+ )
+ });
+
+ afterEach(() => {
+ api.resetHandlers();
+ });
+
+ afterAll(() => {
+ cleanup();
+ api.close();
+ });
...
});
3 - Tests: Change assertions
And finally, we have to change our assertions, as Testing Library has a much different API than Enzyme does.
test.js
diff
...
it('should exist', () => {
- expect(wrapper.exists(OldClassComponent)).toBe(true);
+ expect(wrapper.container.querySelector("#stretchy-container")).toBeVisible();
});
it('should display the stuff', () => {
- expect(wrapper.exists(JustShowTheStuff)).toBe(true);
+ expect(wrapper.container.querySelector("#stretchy-stuff")).toBeVisible();
});
it('should display the things to do', () => {
- expect(wrapper.exists(ThingsToDoArea)).toBe(true);
+ expect(wrapper.container.querySelector("#stretchy-things")).toBeVisible();
});
});
And with that, we have our refactored test!
Aside: It’s worth noting that using
.querySelector
is not high on the list of prioritized queries suggested by Testing Library’s maintainers; in fact, they refer to it as an ‘escape hatch'.However, we have made a conscious decision to use
.querySelector
over test IDs, as test IDs will not (or at least should not) be present on elements in production code while any queries used by.querySelector
and.querySelectorAll
should still return the same results if run in a user’s browser.All of that being said, I’m opting to use
.querySelector
in these examples for the sake of clarity between the test assertions and the component code. Following the suggestions by Testing Library, though, you should always try to use queries fromscreen
whenever possible, as those queries more closely align with Testing Library’s Guiding Principles.
Before/After
OldClassComponent
test
NewClasslikeComponent
test
Staying the Course
I’m really stretching it with these headings, huh?
But continuing with this convoluted-and-probably-confusingly-incorrect analogy I’ve created, this is the part where we refactor the component.
1 - Component: Change imports
Just like with the tests, let’s start with the imports.
index.jsx
diff
- import React, { Component } from 'react';
+ import React, { useContext } from 'react';
- import PropTypes from 'prop-types';
- import { connect } from 'react-redux';
import styles from './style.css';
- import JustShowTheStuff from 'example/components/JustShowTheStuff';
- import ThingsToDoArea from 'example/components/ThingsToDoArea';
+ import { JustShowTheStuff, ThingsToDoArea } from 'example/components';
- import { collapseStuff, collapseToDoItems, getThingsToDo, loadStuff } from 'example/redux/actions';
+ import { collapseStuff, loadStuff } from 'example/store/stuff/actions';
+ import { collapseToDoItems, getThingsToDo } from 'example/store/thingsToDo/actions';
+ import { StateContext, DispatchContext } from 'example/context';
Aside: You might be yelling “I thought you were going to use styled-compnents!?” but fear not! We’ll get there. ð
2 - Component: Change declaration and replace constructor
Next, let’s change the component declaration and change the constructor
to something that will actually work in a functional component.
index.jsx
diff
- class OldClassComponent extends Component {
- constructor(props) {
- super(props);
- this.handleEscKeyDown = handleEscKeyDown.bind(this)
- this.handleStuffClick = handleStuffClick.bind(this)
- this.handleOnSubmit = handleOnSubmit.bind(this)
- this.handleToDoItemClick = handleToDoItemClick.bind(this)
- }
+ const NewClasslikeComponent = () => {
+ const { stuff, thingsToDo, user } = useContext(StateContext);
+ const dispatch = useContext(DispatchContext);
+
+ function handleEscKeyDown(event) {
+ if (event.keyCode === 27) {
+ collapseStuff({ dispatch });
+ collapseToDoItems({ dispatch });
+ }
+ }
+
+ function handleStuffClick(e) {
+ // do some stuff
+ }
+
+ function handleOnSubmit(e) {
+ // do some stuff
+ }
+
+ function handleToDoItemClick(e) {
+ // do some stuff
+ }
...
};
- function handleEscKeyDown(event) {
- if (event.keyCode === 27) {
- this.props.collapseStuff();
- this.props.collapseToDoItems();
- }
- }
-
- function handleStuffClick(e) {
- // do some stuff
- }
-
- function handleOnSubmit(e) {
- // do some stuff
- }
-
- function handleToDoItemClick(e) {
- // do some stuff
- }
-
- OldClassComponent.defaultProps = {
- stuff: null,
- stuffSlug: '',
- things: null,
- toDoItemId: ''
- };
-
- OldClassComponent.propTypes = {
- stuff: PropTypes.object,
- stuffSlug: PropTypes.string,
- thingsToDo: PropTypes.array,
- toDoItemId: PropTypes.string,
- userHandle: PropTypes.string.isRequired,
- };
-
-
- function mapStateToProps(state) {
- return {
- stuff: state.stuff.data,
- stuffSlug: state.stuff.id,
- thingsToDo: state.thingsToDo.data,
- toDoItemId: state.thingsToDo.itemSlug,
- userHandle: state.user.handle
- };
- };
-
- const mapActionsToProps = {
- collapseStuff,
- collapseToDoItems,
- getThingsToDo,
- loadStuff,
- };
-
- export default connect(mapStateToProps, mapActionsToProps)(OldClassComponent);
+ export default NewClasslikeComponent;
Some of these changes are pretty straightforward: the class
becomes a function
(or in this case, an arrow function), removed the constructor, changed the default export, and moved the event handlers into the component declaration itself (this is more of a preference). However, the Redux-related changes may be less obvious.
First of all, connect
is unique to Redux so we obviously have to get rid of that if one of our overall goals is to remove Redux entirely. But what about mapStateToProps
and mapActionToProps
? Well thanks to the useContext
hook, we can, within our component, get both state values directly and access to the dispatch
function, through which we will dispatch actions. Therefore, we no longer need to pass them into the component as props
like Redux does.
3 - Component: Replace lifecycle methods with hooks
The next big change is replacing the lifecycle methods with their Hooks API equivalents. Let’s start with the one that you’ll often find onboard: useEffect
.
index.jsx
diff
...
- componentDidMount() {
- this.props.loadStuff(
- // this.props.stuffSlug
- this.props.userHandle
- )
-
- this.props.getThingsToDo(
- // this.props.toDoItemId
- this.props.userHandle
- )
-
- window.addEventListener('keydown', this.handleEscKeyDown)
- }
-
- componentWillUnmount() {
- window.removeEventListener('keydown', this.handleEscKeyDown)
- }
+ useEffect(() => {
+ if (stuff.data == null) {
+ loadStuff({ dispatch, userHandle: user.handle })
+ }
+
+ if (thingsToDo.data == null) {
+ getThingsToDo({ dispatch, userHandle: user.handle });
+ }
+
+ window.addEventListener('keydown', handleEscKeyDown);
+
+ return () => {
+ window.removeEventListener('keydown', handleEscKeyDown)
+ }
+ }, []);
...
As useEffect
replaces componentDidMount
, componentDidUpdate
, and componentWillUnmount
, it’s quite the heavy-hitter in the Hooks API. Additionally, you can have multiple useEffect
hook calls inside components, which helps encapsulate discreet pieces of logic inside of individual calls.
Aside: In addition to the React docs themselves, this article by John Au-Yeung is incredibly helpful and concise!
There’s still one more lifecycle method to replace, render
, but this one’s replacement is really easy to remember: just a plain ol’ return
. ð
index.jsx
diff
...
- render() {
- const canRenderStuff = this.props.stuff != null && typeof this.props.stuff === "object";
+ const canRenderStuff = stuff.data != null && typeof stuff.data === "object";
- const canRenderThingsToDo = this.props.thingsToDo != null && Array.isArray(this.props.thingsToDo);
+ const canRenderThingsToDo = thingsToDo.data != null && Array.isArray(thingsToDo.data);
return (
- <div className={styles.stretchyLikeSuspenders}>
+ <div id="stretchy-container" className={styles.stretchyLikeSuspenders}>
<h2 className={styles.bolderPlease}>Look at this dashboard!</h2>
{canRenderStuff ? (
- <>
+ <div id="stretchy-stuff">
<h3 className={styles.goodOlHeading}>Look at all this stuff!</h3>
<JustShowTheStuff
- theStuff={this.props.stuff}
+ theStuff={stuff.data}
- handleStuffClick={this.handleStuffClick}
+ handleStuffClick={handleStuffClick}
- handleSubmit={this.handleSubmit}
+ handleSubmit={handleOnSubmit}
/>
- </>
+ </div>
)}
{canRenderThingsToDo && (
- <>
+ <div id="stretchy-things">
<h3 className={styles.goodOlHeading}>Look at all these things to do!</h3>
<ThingsToDoArea
- things={this.props.thingsToDo}
+ things={thingsToDo.data}
- handleToDoItemClick={this.handleToDoItemClick}
+ handleToDoItemClick={handleToDoItemClick}
/>
- </>
+ </div>
)}
</div>
)
- }
};
Before we swap out the CSS modules for styled-components
, here’s a look at the changes side-by-side.
Before/After
OldClassComponent
NewClasslikeComponent
Home Stretch
With the bulk of the work behind us, let’s wrap things up by addressing our final goal: replacing CSS modules with styled-components
.
Of course, there’s nothing wrong with using CSS modules: it’s one of many options for styling components and may be the ideal choice in some cases and/or for some teams. In our case, however, we needed more flexibility when it came to applying styles, especially in situations where we may need to change styling based on a component’s particular context. Aside from that, it provided us with some other benefits that we were more than willing to take. ð
As a reminder, here is our CSS for the OldClassComponent
.
index.css
.stretchyLikeSuspenders {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: flex-start;
}
.goodOlHeading {
color: #ccc;
font-size: 18px;
font-weight: 400;
line-height: 1.33;
}
.bolderPlease {
font-weight: 700;
}
1 - Styles: Change imports
Once again, changing imports is the easiest place to start. Unlike in the other sections, however, the import changes here are very small and we will also have to change references to the classes into strings.
index.jsx
diff
import React, { useContext } from 'react';
+ import styled from 'styled-components';
import { JustShowTheStuff, ThingsToDoArea } from 'example/components';
import { collapseStuff, loadStuff } from 'example/store/stuff/actions';
import { collapseToDoItems, getThingsToDo } from 'example/store/thingsToDo/actions';
import { StateContext, DispatchContext } from 'example/context';
- import styles from './style.css';
const NewClasslikeComponent = () => {
...
return (
- <div id="stretchy-container" className={styles.stretchyLikeSuspenders}>
+ <div id="stretchy-container" className="stretchyLikeSuspenders">
- <h2 className={styles.bolderPlease}>Look at this dashboard!</h2>
+ <h2 className="bolderPlease">Look at this dashboard!</h2>
{canRenderStuff && (
<div id="stretchy-stuff">
- <h3 className={styles.goodOlHeading}>Look at all this stuff!</h3>
+ <h3 className="goodOlHeading">Look at all this stuff!</h3>
<JustShowTheStuff
theStuff={stuff.data}
handleStuffClick={handleStuffClick}
handleSubmit={handleSubmit}
/>
</div>
)}
{canRenderThingsToDo && (
<div id="stretchy-things">
- <h3 className={styles.goodOlHeading}>Look at all these things to do!</h3>
+ <h3 className="goodOlHeading">Look at all these things to do!</h3>
<ThingsToDoArea
things={thingsToDo.data}
handleToDoItemClick={handleToDoItemClick}
/>
</div>
)}
</div>
);
};
2 - Styles: Adding styled components
While there are several useful APIs in the library, the primary styled
API from styled-components
is simple both to use and incorporate into any component.
index.jsx
diff
...
+ const StyledStretchyContainer = styled.div`
+ align-items: flex-start;
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: space-between;
+ `;
+
+ const StyledBolderPlease = styled.h2`
+ font-weight: 400;
+ `;
+
+ const StyledGoodOlHeading = styled.h3`
+ color: #ccc;
+ font-size: 18px;
+ font-weight: 400;
+ line-height: 1.33;
+ `;
const NewClasslikeComponent = () => {
...
return (
- <div id="stretchy-container" className="stretchyLikeSuspenders">
+ <StyledStretchyContainer id="stretchy-container" className="stretchyLikeSuspenders">
- <h2 className="bolderPlease">Look at this dashboard!</h2>
+ <StyledBolderPlease className="bolderPlease">Look at this dashboard!</StyledBolderPlease>
{canRenderStuff && (
<div id="stretchy-stuff">
- <h3 className="goodOlHeading">Look at all this stuff!</h3>
+ <StyledGoodOlHeading className="goodOlHeading">Look at all this stuff!</StyledGoodOlHeading>
<JustShowTheStuff
theStuff={stuff.data}
handleStuffClick={handleStuffClick}
handleSubmit={handleSubmit}
/>
</div>
)}
{canRenderThingsToDo && (
<div id="stretchy-things">
- <h3 className="goodOlHeading">Look at all these things to do!</h3>
+ <StyledGoodOlHeading className="goodOlHeading">Look at all these things to do!</StyledGoodOlHeading>
<ThingsToDoArea
things={thingsToDo.data}
handleToDoItemClick={handleToDoItemClick}
/>
</div>
)}
- </div>
+ </StyledStretchyContainer>
);
};
export default NewClasslikeComponent;
Additionally, the styles.css
file can be deleted entirely as we no longer need it to style our component. We could even stop right here, if we wanted; functionally, there’s nothing left for us to do. ð
3 - Styles: ðĩ Clean up, clean up! ðĩ
All of that said, however, we noticed early on that leaving the styled components in the index.jsx
component file could often make the component code feel cluttered; instead of those files just containing each component’s function, they now contained dozens of lines of what is essentially CSS, as well.
As far as I know, there isn’t really any one “right” way to address this problem; if asked, I suspect the Internet would provide a myriad of solutions. In our case, we decided to move our styled components into a styled.jsx
file and then import them in the main index.jsx
file.
styled.jsx
diff
+ import styled from 'styled-components';
+
+ const StyledStretchyContainer = styled.div`
+ align-items: flex-start;
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: space-between;
+ `;
+
+ const StyledBolderPlease = styled.h2`
+ font-weight: 400;
+ `;
+
+ const StyledGoodOlHeading = styled.h3`
+ color: #ccc;
+ font-size: 18px;
+ font-weight: 400;
+ line-height: 1.33;
+ `;
+
+ export { StyledStretchyContainer, StyledBolderPlease, StyledGoodOlHeading };
index.jsx
diff
import React, { useContext } from 'react';
- import styled from 'styled-components';
import { JustShowTheStuff, ThingsToDoArea } from 'example/components';
import { collapseStuff, loadStuff } from 'example/store/stuff/actions';
import { collapseToDoItems, getThingsToDo } from 'example/store/thingsToDo/actions';
import { StateContext, DispatchContext } from 'example/context';
+ import { StyledStretchyContainer, StyledBolderPlease, StyledGoodOlHeading } from './styled';
- const StyledStretchyContainer = styled.div`
- align-items: flex-start;
- display: flex;
- flex-flow: row wrap;
- justify-content: space-between;
- `;
-
- const StyledBolderPlease = styled.h2`
- font-weight: 400;
- `;
-
- const StyledGoodOlHeading = styled.h3`
- color: #ccc;
- font-size: 18px;
- font-weight: 400;
- line-height: 1.33;
- `;
const NewClasslikeComponent = () => {
...
};
export default NewClasslikeComponent;
Out with the Old, In with the New!
Now that we’ve migrated and refactored our tests, component, and styles, let’s have one final look before docking at the proverbial harbor.
Before/After
OldClassComponent
NewClasslikeComponent
Shore Leave: Final Thoughts
We made it. We really, really made it.
With our journey completed, we have successfully migrated one of our components.
Now instead of simply uninspiredly recapping what all we set out to accomplish, let’s talk about something more juicy: the yet-to-be-covered takeaways from this whole migration process.
1. Prepare and Document Decisions
Ok, this one seems obvious but in all seriousness, its value really can’t be understated.
Before even beginning work, I would suggest at least asking yourself (or -selves) the following questions and documenting your answers somewhere.
- What Node version is your frontend currently using? For whatever reason, do you need to continue using that version?
- Does our frontend use a framework? If so, which one and what version?
- Are there any notable libraries we need to consider, whether they be supporting libraries for that framework or just widely used in your frontend?
- What does your destination look like? Are you sticking with whatever framework you’re using, moving to another, or something else?
- If usage of any of those notable libraries would be impacted and/or you’d like to remove them entirely, what would that look like? If you’re removing one or more, why? And, if applicable, what is your plan to replace them?
Migrations like ours are also an amazing opportunity to address some far-reaching tech-debt and maintenance items. For instance, we used this migration to remove/replace some unwanted dependencies (for example, Redux -> Hooks API and Enzyme -> Testing Library). Of course, you and/or your team won’t always have the opportunity to tackle such work during a migration but it’s worth reflecting on whether that is something to include in your plan. If tech-debt/maintenance work makes it into your plan, then this planning phase is the perfect time to reasonably codify what you will and won’t be doing.
Where and how you document them is less important, as long as the location, medium, and level of clarity make sense for you and/or your team. If working with a team, make sure you all share (or at least think you all share) an understanding of the document.
2. Develop a Plan of Attack
The specifics of your plan aren’t really as important as having a plan at all.
In our case, the plan was pretty high-level:
- migrate static components first
- migrate as many utils and/or boilerplate as possible
- work your way through the stateful components, starting with those that have the fewest and/or simplest dependencies
And that might be all you need! Or maybe you/your team would prefer to have something more fleshed out and defined; that’s ok too!
The most important thing is to have an idea of how you intend to get from point A to point B with the least amount of friction.
3. Keep the Work Simple
Be disciplined about keeping pieces of work contained to a single component.
No seriously, be disciplined about keeping pieces of work contained to a single component.
The more that you can abide by the “1-ticket:1-component” rule, the easier it will be to review and test each set of changes. It will also likely make the work feel more approachable and fulfilling; as a side effect of adhering to this rule, you will be breaking a large and somewhat-daunting task into smaller pieces of work, some of which can easily be completed in a single sitting. This can be especially beneficial in helping to keep you and/or the team motivated, since the satisfaction of completing something is sure to give folks well-deserved boosts of dopamine as you gradually progress through the migration.
While there are surely other benefits to keeping the work simple, it will undoubtedly help you/your team to…
4. Systematize the Work
I mean, hell, how do you think I was able to write this blog post!? ðĪŠ
At first, I would not expect that you and/or your team would have developed a system for the work. Personally, however, I found that within a half-dozen components or so, I had largely developed my own system and it really accelerated the pace of the work.
A huge part of systematizing the work is trying to identify patterns early. I’m not sure that there’s any sort of playbook for how to do that but I can absolutely say that it requires being mindful while you’re working so that you can readily notice similarities between the work for a component with each other piece of completed work.