My first portfolio with React and AWS
Tags: showdev, react, aws, frontend
Published: 2018-07-29 01:58:17

---
title: My first portfolio with React and AWS
published: true
description: Going down the rabbit hole
tags: #showdev, #reactjs, #aws, #frontend
---

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](https://reactjs.org/docs/context.html), 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.
```javascript
//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. 

```jsx
// 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. 
```jsx
// 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:

```jsx
// 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](https://cloudinary.com/), 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](https://caniuse.com/#feat=intersectionobserver) 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](https://emotion.sh)**, 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](https://reactjs.org/docs/create-a-new-react-app.html)
- [Emotion](https://emotion.sh) (css-in-js)
- [Firebase](https://firebase.google.com/) (for the contact form)
- [AWS S3 bucket](https://aws.amazon.com/es/s3/) (static files hosting)
- [AWS Cloudfront](https://aws.amazon.com/es/cloudfront/) (CDN, SSL Certificate, text compression)
- [AWS Route53](https://aws.amazon.com/es/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](https://developers.google.com/web/tools/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](https://aws.amazon.com/es/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](https://gist.github.com/bradwestfall/b5b0e450015dbc9b4e56e5f398df48ff) 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](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Expiration.html).

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](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html) 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:
![aws billing](https://thepracticaldev.s3.amazonaws.com/i/ud902i91parjgi8ga9h8.JPG)

*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.