Before moving on, this is a written version of a talk I gave about the same subject, you can see the presentation and full code example here: https://github.com/joepurnell1/sharing-components-talk
You're about to see I've made some assumptions on technologies used, this is made from part preference and part problem solving. For this blog post I'm assuming you love Styled-Components, React, and React-Native.
Part 1: Atomic Design
So starting at the top, Atomic Design. This is a great design methodology created by the infamous Brad Frost. You can read about it more in-depth at his great blog here: https://bradfrost.com/blog/post/atomic-web-design/
The biggest take away we use here is the component structure, Brad breaks down UI structures into 5 layers:
Atoms
Your lowest level, dumb components - think your basic image component which can be used repeatedly:
...
export const Image = ({ source, height, width, ...props }) => (
<ImageElement src={source} height={height} width={width} {...props} />
);
Molecules
This is where we start pulling our Atoms together to form a bit of a structure, here we can take our image component, pair it with a text component to make this song listing component:
import { Image } from "../../atoms/image";
import { Title } from "../../atoms/typography";
...
export const SongListing = ({ imageSrc, songTitle, onPress }) => {
const handlePress = () => {
onPress(songTitle);
}
return (
<ClearButton margin="16px 32px 0 32px;" width="80%" onPress={handlePress}>
<Container>
{imageSrc && <Image source={imageSrc} height="48px" width="48px" />}
<SongTitle>{stringShortener(`${songTitle}`, 30)}</SongTitle>
</Container>
</ClearButton>
);
};
Organisms
Here is where it starts getting interesting, we can pull our Atoms and Molecules together in larger components for our pages, still in a reusable way. You can consider a form component, or this handy album list component:
import { Title } from '../../atoms/typography';
import { SongListing } from '../../molecules/songlisting';
...
export const AlbumList = ({ albumArt, songList = [], artist, onSongPress }) => {
return (
<Container>
{
songList.map(songTitle => <SongListing key={songTitle} imageSrc={albumArt} songTitle={`${songTitle} - ${artist}`} onPress={onSongPress} />)
}
<Title>no more songs in album</Text>
</Container>
);
}
Templates
So now we've defined the elements of our pages, templates define the overall structure of your page without any defining state management or event handling.
This is where a combination of components come together to form the look and feel of a page.
You can think of this like a blog post structure without the real words on the page and replaced with placeholders (or nothing).
Pages
This is where our structure comes to life. We can now implement our controlling handlers and state management. So in short, Pages provide the thought behind the UI.
Part 2: Molecular Bonds
Molecular Bonds (or Covalent Bonds) are chemical bonds which hold atoms together (pretty fancy).
Here we can consider our container components (our sections, divs, pages etc) as our molecular bonds. So why not define our layout components:
export const horizontalView = styled.div`
display: inline-flex;
align-items: center;
justify-content: center;
`;
This would then give our components a slightly different structure, if we consider our molecule structure with it's own defined layout component, it would look something like this:
The simple change here is that the div get's replaced with our new horizontalView component, this may seem like a small or insignificant change to point out, but we get a big benefit for sharing components as you will see later on.
You're probably thinking we're going to end up with loads of different or even quite complicated layout components. In a way, you're correct. But that isn't such a bad thing, defining the behaviour of your layout components will help keep a consistent layout styles across your site as you can define spacing. We can also consider the use of props to limit the number of duplicated layout components.
Part 3: Build Orchestration
There are two parts to this we need to look at?
What builds our web project?
In short, Webpack. Luckily for us, our needed functionality comes for free out the box: Webpack looks for javascript files containing our code. If it doesn't find a valid file we hit a bit of a snag and the build fails.
Webpack will import all of the appropriate files to use for the web during bundling so our albumList component will look like this for web:
What builds our native project?
Metro Bundler steps in here, it gathers our javascript resources together and serves them to the application. The process Metro Bundler uses to find files is what's interesting here:
- Metro Bundler will look for a platform specific file for a component (i.e. index.ios.js)
- Failing that, it will look for a valid cross platform file (index.native.js)
- If both cannot be found it will result to looking for any compatible for a component (index.js)
So, using the same albumList component with our abstracted atoms and bonds, we will see a structure like this for iOS as metro bundler kicks in and bundles our component together for us:
Our component looks remarkably similar, but we can see our atoms and layout components have been chosen using the platform specific file types.
Part 4: Why Styled-Components?
With all this sharing of components we want to keep the language of our components clear and reusable. Styled-Components (with it's web and native support) is perfect for this task. Let's have a look at our typography component:
// typography/index.js
import styled from 'styled-components';
export const Title = styled.p`
font-size: 16px;
font-family: sans-serif;
font-weight: 600;
`;
Here we create a simple web component, Title, and we apply some styling to it. Now if we wanted to define the same component for React Native using styled-components we would end up with this:
// typography/index.native.js
import styled from 'styled-components/native'; // different import
export const Title = styled.Text`
font-size: 12; {/* different font-size */}
font-family: sans-serif;
font-weight: 600;
`;
They're looking pretty similar right? The structure is the same, the only real differences are the font-sizes (which we could make more flexible using theming) and the import statement.
This shared use of a similar styling language is handy as this allows us to simplify our development experience, we also get the added bonus of being able to expand on base styling in higher up components as we're about to see.
Part 5: Sharing Code
We've seen how the bundlers pick up our file types for use across platforms but by structuring our UI components to abstract away as much as possible we can then share components at a higher level all from a single file.
Let's look again at our Title component from above, if we were to then go ahead and make a new component (fancyComponent) we can define a single file:
// fancyComponent/index.js
import { Title } from './typography'
const RedTitle = styled(Title)`
color: red;
`;
This new fancy component, between it's builder and styled-components, would work on both React Native and React web projects since we've defined its children for both native and web and our builders will select their appropriate file type.
On top of that, we would see the same color: red;
styling take effect on all platforms as we're using styled-components across both platforms. So our component will look like this:
Finale
So there we have it, we've managed to define a component and use a single file across React and React Native, without the need for anything super fancy. Now all we need to do is add this component into our component library module and use it for our different platforms.
It's important to note a few things:
- Just because we can share code, doesn't mean we always should
- This method of development expects there to be many components, and thats ok! Just reuse when you can or consider making a component more adaptable before making a new one (maybe our RedTitle component could've been a colour prop added to the base level Title component)
- This way of working work works better when the same dev/team are making changes across all platforms
Well, that's it from me. If you want to reach out, please do. As always, this isn't the only way to do this kind of work, just the way that I have.
Top comments (2)
Hi Joe,
you wrote "infamous Brad Frost" while presenting his "Atomic Design" methodology as a "great one". "Infamous" means someone "famous" for having done something bad. So it strikes me as a mistake. Was it intentional?
Hi Joe do you have an example repo to show ?