My first portfolio with React and AWS

georginagrey

Georgina Grey

Posted on July 29, 2018

My first portfolio with React and AWS

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: () => { }
});

Enter fullscreen mode Exit fullscreen mode

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>
    )
}

Enter fullscreen mode Exit fullscreen mode

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>
)
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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.

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:
aws billing

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.
💖 💪 🙅 🚩
georginagrey
Georgina Grey

Posted on July 29, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related