DEV Community

Georgina Grey
Georgina Grey

Posted on • Updated on

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.

Latest comments (41)

Collapse
 
ernestochavesch profile image
Ernesto Chaves

Awesome job.

Collapse
 
georginagrey profile image
Georgina Grey

Thank you for checking it out! :)

Collapse
 
josechavarriacr profile image
Jose Chavarría

Demasiado Chiva te quedó !

Collapse
 
georginagrey profile image
Georgina Grey

Hey, muchas gracias :)

Pura vida!

Collapse
 
dukemai profile image
dukemai

Hey Georgina, your site is beautiful and creative. thanks for your sharing your work.

Collapse
 
georginagrey profile image
Georgina Grey

Hi! Thank you very much 🤗

Collapse
 
shackra profile image
Jorge Araya

Se ve chiva la vara, pv!

Collapse
 
georginagrey profile image
Georgina Grey

Gracias! 😎

Collapse
 
kevinwolfdev profile image
Kevin Wolf

Hey!

Congratulations, nice to see people from my same country doing nice things!, it looks like you are going the right way and using the right tools (although I would have chosen Netlify).

Now that you are diving into front end development I would recommend you to take a look at css grid (cssgridgarden.com/#es).

Any react question you have, you are more than welcome to ping me at GitHub or Twitter.

Pura vida!

Collapse
 
georginagrey profile image
Georgina Grey

Thank you! That looks cool, I will start gardening soon enough 😁🌱

Saludos!

Collapse
 
petermcteague profile image
Peter Mcteague

It looks really good! Great job! As someone who's just getting into frontend development I think your portfolio does a lot of things right and it's provided me with some inspiration.

Collapse
 
georginagrey profile image
Georgina Grey

Thank you Peter! There's some edge cases I'll be working on, but I'm happy you can get inspired from it :)

Collapse
 
ekafyi profile image
Eka

Nice post describing your work process! Just would like to point out the footer is displayed oddly on Chrome 67, OS X 10 -- thepracticaldev.s3.amazonaws.com/i...

Collapse
 
georginagrey profile image
Georgina Grey

You're right, I moved the footer outside it's original parent while testing something else, so it broke :P

Thank you. I'll be pushing the CDN Invalidation in a bit.

Collapse
 
sarah_chima profile image
Sarah Chima

Hey Georgina, your portfolio looks beautiful. I love the layout.

In the "Let's talk" section, you can try increasing the height of your input fields and the font-size of the text since there are no labels. A good rule is always to add labels though. It's great nevertheless.👍

Collapse
 
georginagrey profile image
Georgina Grey

You're right, small details I tend to miss sometimes. Thanks for bringing it up!

Collapse
 
sarah_chima profile image
Sarah Chima

You are welcome

Collapse
 
squidbe profile image
squidbe

Just one minor correction: it's TTL (time to live) rather than TLL.

Collapse
 
georginagrey profile image
Georgina Grey

Of course! I fixed it, thanks!

Collapse
 
sandywilkins profile image
sandywilkins • Edited

Hola! Qué bueno encontrar una tica por aquí! Me pasas los recursos que le mencionas a Chad de trabajo remoto (el email es mi usuario aquí + "cr" al final, at gmail)? Pequeño typo: "trabajando en bases de daños". Pura vida!

Collapse
 
georginagrey profile image
Georgina Grey

Hola! Claro voy a buscarlos. Jaja a pesar de haber leído eso mil veces no sé cómo se me escapó, gracias. Al rato fue un mensaje del subconsciente 😂 saludos!

Collapse
 
sandywilkins profile image
sandywilkins

😆

Collapse
 
ionutmilica profile image
Ionut Milica • Edited

Good work! 👏
One thing you may want to add is a IntersectionObserver polyfill if you plan to support non Chrome/Firefox browsers 😄

Collapse
 
georginagrey profile image
Georgina Grey

I knew I was forgetting something! Thanks for the reminder.

Collapse
 
kylegalbraith profile image
Kyle Galbraith

Great write up Georgina. If you are looking to expand on this to learn more AWS, check out my book or feel free to ping me with any questions.

Collapse
 
georginagrey profile image
Georgina Grey

Thank you Kyle, this is great, AWS can still get a bit overwhelming at times. I'll look into it.

Collapse
 
zetamorph profile image
Johannes Loewe

Looks nice 👍

However I'm on mobile(Chrome on an iPhone) and clicking on "experience" completely breaks the site to the point of having to refresh it.

Collapse
 
georginagrey profile image
Georgina Grey

Oh! That ain't good. I'll see if I can reproduce it, thanks a lot!

Collapse
 
aymanazm profile image
Ayman Azm

Amazing and seamless

Collapse
 
nosykretts profile image
Fajar Andi Patappari

Hey congratulation on your first portofolio. It looks beautifull.

Collapse
 
georginagrey profile image
Georgina Grey

Hi Fajar! Thanks for checking it out :)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.