Georgina Grey
Posted on July 29, 2018
Hi dev.to! So, I built my first portfolio and thought about documenting the process, but before jumping in, a disclaimer:
- I believe in choosing the right tool for the job, using React for a portfolio might seem like an overkill but I decided on it mostly because I want to get better at it.
- For that same reason I chose AWS to deploy it instead of Github or Netlifly. AWS is a beast and I want to learn as much as I can.
Phew! Okay, so let's get to it. Oh, here's the portfolio https://georginagrey.com
The interesting bits
When coding the app I learned a few new tricks that I believe are worth sharing.
React's Context API
My portfolio is multi-language, to achieve that I used React's Context, the point is to have a sort of "global" state that can be accessed by other components that could be deeply nested, thus avoiding passing props many levels down the chain. This is how it helped me implement the language switcher:
Provider
On LanguageContext.js is where the text translations live and the Context is created and exported.
//LanguageContext.js
export const languages = {
en: {...
},
es: {...
}
}
export const LanguageContext = React.createContext({
langText: languages.en,
toggleLanguage: () => { }
});
The App component is the most outer component, where the toggleLanguage function is actually implemented. LanguageContext.Provider component wraps every other children that needs to consume the "global" state.
Watch out when sharing functions that access state, such functions need be explicitly binded to state, by either using the super(props) keyword or the bind(this) method, otherwise components nested deep down executing this function will throw an error.
// App.js
...
import { LanguageContext, languages } from './LanguageContext';
...
constructor(props) {
super(props);
this.state = {
language: 'en',
langText: languages.en,
toggleLanguage: this.toggleLanguage
}
}
toggleLanguage = () => {...
}
render() {
return (
<div id="app" className={app}>
<LanguageContext.Provider value={this.state}>
<Menu />
<Main />
<Footer />
</LanguageContext.Provider>
</div>
)
}
Consumer
The LanguagePicker component is nested about 3 levels deep, thanks to the LanguageContext.Consumer component, this is how state can be accessed.
// LanguagePicker.js
const LanguagePicker = () => (
<LanguageContext.Consumer>
{({ toggleLanguage, language }) => (
<div className={main} onClick={() => toggleLanguage()}>
...
<span>{language}</span>
</div>
)}
</LanguageContext.Consumer>
)
This could have been achieved with Redux too, but I didn't need it for anything else. The Context API shouldn't be used lightly though, so keep that in mind.
Intersection Observer API
It's very useful if a behavior needs to be triggered when some element is visible inside the viewport. I used it to trigger some animations, but the most meaningful use case has to do with improving the site's load time, first contentful paint and lower bandwidth usage.
The <img>
tag renders right away whatever's in its source, even if the component hasn't mounted yet, so the user will download images that might never even get to see. A slow down in the first contentful paint is also expected.
The trick here is to use a placeholder, taking the original image and scaling it down to a ~10x10 pixel ratio. Is only when the IntersectionObserver kicks in, that we fetch the original image. Here's a snippet of the implementation:
// Proyects.js
componentDidMount() {
this.observe();
}
observe() {
var options = {
threshold: [0.1]
}
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
const image = entry.target;
const src = image.dataset.src;
this.fetchImage(src).then(() => {
image.src = src;
});
}
});
}, options);
const images = document.querySelectorAll('img');
images.forEach(i => observer.observe(i));
}
Pro tip: instead of scaling down the images myself I used Cloudinary, you can transform images on the fly when the c_scale is provided within the url:
https://res.cloudinary.com/georginagrey/image/upload/c_scale,h_12,w_12/v1532709273/portfolio/portfolio.jpg
, if you take that bit off, you get the original image.
Heads up: The IntersectionObserver it's is not entirely supported across all browsers, so you might want to use a pollyfill or a fallback.
The UI
This is my weakest spot, it wasn't until recently that I kinda got my head around CSS3, or that's what I thought until I started to fall in every "gotcha" possible when styling components using just plain CSS. I had to re-write the whole thing a couple of times, until I decided to use emotion, even though css-in-js causes some outrage, I decided to give it a go and I loved it, I no longer have to worry about overriding rules while working on different components.
The layout is quite simple, I went with a mobile-first approach, and got away with using flexbox only.
The Stack
In a nutshell, this is a React static website hosted on a S3 bucket served by CloudFront and Route53.
- Create-react-app
- Emotion (css-in-js)
- Firebase (for the contact form)
- AWS S3 bucket (static files hosting)
- AWS Cloudfront (CDN, SSL Certificate, text compression)
- AWS Route53 (DNS routing)
How did I end up with that?!
After writing the main React components and styling most of them, I stumbled with Google's Lighthouse auditing tool, I downloaded the Chrome extension and generated a report (locally) and within seconds I got the results and a list of opportunities for optimization, for example, by enabling "text compression" in the server, the app should load about 3 seconds faster in my case.
I didn't know what that meant, so after googling for a bit I came across Cloudfront, to top it off you can request a SSL certificate for free.
Setting everything up is not as difficult as it may sound, here is a very handy guide. What will you get? Hosting, increased performance, faster delivery and secure HTTPs.
Is it free?
S3 and CloudFront are not per se free, is pay-as-you-go service, so for a low traffic website we would be talking about paying cents per month if anything at all, after the 1 year free tier expires.
Route53 is the DNS provider, there's a fixed price of $0.51/month per hosted zone, so we're talking only about $6/year. In this I case I already had a domain registered in Godaddy, to make it work I just grabbed the DNS names Route53 provided me with and saved them inside the Manage Name Servers form in Godaddy.
Caching and invalidating CloudFront
As is expected, every time a request comes into CloudFront it will serve whatever is cached instead of going every time to your S3 bucket looking for the files, how long the content stays cached, depends on the default TTL timeframe configured, read more about it here.
Since I'm still working on the site, I set the default TTL to
3600 seconds (1 hour), I also added a header cache-control:max-age=0
, to the meta-data of the origin S3 bucket. But soon I'll be reverting that and use Invalidation instead, it force flushes the cache without needing to wait for it to expire. Doing it this way is actually cheaper too.
Edit:
I got my monthly statement! So, here's an example of AWS Princing with this setup:
CloudFront served +2300 requests for America/Europe tier. Plus DNS routing and storage for $0.62 total. It won't get more expensive than that since a surge in traffic is not expected.
That's it! ... I think 🤔
This isn't my first time dealing with AWS, but it's my first coding the front-end of a website, so any comments are greatly appreciated.
Thank you for stopping by 👋
Edit 07/30: Added warning about IntersectionObserver.
Edit 08/03: Added AWS Billing Statement.
Posted on July 29, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.