<![CDATA[Vanja's ramblings on tech]]>https://vanja.gavric.org/blog/https://vanja.gavric.org/blog/favicon.pngVanja's ramblings on techhttps://vanja.gavric.org/blog/Ghost 2.37Tue, 14 Jan 2025 22:19:42 GMT60<![CDATA[Binge Watch Time is live!]]>Over the years, I've used many TV show tracking apps but was never fully happy with them. To change that, I've decided to make one myself.

Without further ado, let me present you my puppy - Binge Watch Time.
BWT is a Node/React/Redux SPA. It uses the amazing

]]>
https://vanja.gavric.org/blog/binge-watch-time-is-live/678622b07de7d4d0ebc3317bTue, 14 Jan 2025 08:41:16 GMT

Over the years, I've used many TV show tracking apps but was never fully happy with them. To change that, I've decided to make one myself.

Without further ado, let me present you my puppy - Binge Watch Time.
BWT is a Node/React/Redux SPA. It uses the amazing themoviedb.org API for shows data and a MySQL database for local persistence for the app data (starred shows and watched episodes).

BWT has a discovery section by different criteria such as New, Popular overall, or Popular by genres. Opening a show reveals an overview with show info including the show trailer, all episodes grouped by seasons, and the next episode if you are already tracking this show. Shows can be tracked in three lists:

  1. To-Do - for shows discovered by the app, referred by a friend, etc.
  2. Watching - currently being watched (this list will be used for notifications once they are implemented)
  3. Watched - list of fully watched TV shows that can then be used as a reference list for recommendations to friends

BWT is still very much an MVP and there are many features I'd like to get implemented in the future:

  • notifications (email, push,...)
  • better email templates
  • movie support
  • user settings
  • offline support
  • sort shows in lists
  • ...

BWT is open-source, available on Github, and free to use.

In and out. Ciao!

Binge Watch Time is live!
]]>
<![CDATA[Parse URLs in browser without extra dependencies]]>

Or how to win without extra bundle bloat 😉

Parsing URLs in Node.js is easy - just use the built-in URL module.
On the front-end things get more interesting as it seems that, sigh, yet another dependency is needed. Turns out, it is quite easy to accomplish this on this

]]>
https://vanja.gavric.org/blog/parse-urls-in-browser-without-extra-dependencies/678621af7de7d4d0ebc33150Mon, 19 Feb 2018 03:22:00 GMTParse URLs  in browser without extra dependencies

Or how to win without extra bundle bloat 😉

Parsing URLs in Node.js is easy - just use the built-in URL module.
On the front-end things get more interesting as it seems that, sigh, yet another dependency is needed. Turns out, it is quite easy to accomplish this on this end too. I also discovered this by pure accident 💙 although someone stumbling upon this post might be rolling their eyes. 😎

You can use the element <a> to parse various attributes of a URL.

const a = document.createElement('a')
a.href = 'https://user:my-pass@vanja.gavric.org:5000/foo/bar/baz/?foo=bar&baz=qux#hash'

console.log(a.protocol) // 'https:'
console.log(a.username) // 'user'
console.log(a.password) // 'my-pass'
console.log(a.origin)   // 'https://vanja.gavric.org:5000'
console.log(a.hostname) // 'vanja.gavric.org'
console.log(a.port)     // '5000'
console.log(a.host)     // 'vanja.gavric.org:5000'
console.log(a.pathname) // '/foo/bar/baz/'
console.log(a.search)   // '?foo=bar&baz=qux'
console.log(a.hash)     // '#hash'

After reading more about this problem it turns out that there is a URL Web API if the browser supports it (looking at you IE and Edge). And guess what? The syntax is the same as with Node:

const modernA = new URL('https://user:my-pass@vanja.gavric.org:5000/foo/bar/baz/?foo=bar&baz=qux#hash')

console.log(modernA)
// {
//   hash: '#hash',
//   host: 'vanja.gavric.org:5000',
//   hostname: 'vanja.gavric.org',
//   href: 'https://user:my-pass@vanja.gavric.org:5000/foo/bar/,baz/?foo=bar&baz=qux#hash'
//   origin: 'https://vanja.gavric.org:5000',
//   password: 'my-pass',
//   pathname: '/foo/bar/baz/',
//   port: '5000',
//   protocol: 'https:',
//   search: '?foo=bar&baz=qux',
//   searchParams: URLSearchParams,
//   username: 'user'
// }

In and out. Ciao!

Parse URLs  in browser without extra dependencies

]]>
<![CDATA[Integrate Stylelint into Create React App without ejecting]]>https://vanja.gavric.org/blog/integrate-stylelint-into-create-react-app-without-ejecting/678621af7de7d4d0ebc3314fMon, 05 Feb 2018 03:45:00 GMTIntegrate Stylelint into Create React App without ejecting

Or how to heavily modify Create React App settings without having to eject.

I love using Create React App as a starting point for my React front-end as I get to concentrate on code and delivering value. Plus, by updating it I get new features without having to spend hours configuring them myself.
That is, until you want to do something custom with your app that the authors did not intend to. In that case, you have two choices, neither of them being great:

  • eject and expose all logic/dependencies behind react-scripts. This transforms projects into a monstrosity of build settings and dependencies. Then you have to live with it, update CRA yourself, etc because once ejected - there is no going back
  • fork create-react-app and use that as a dependency instead of in your project. You are still left with syncing all the later changes and improvements done on the CRA end. But at least your project is not polluted with everything that happens behind the scene

I wanted to include Stylelint into a project to lint CSS. Basic functionality can be achieved quite easily - yarn add stylelint --dev and add an npm script to your package.json. Something like this:

  "scripts": {
    ...
    "lint:client:css": "stylelint 'src/scss/**/*.scss'",
    ...
  },

This is all sweet and dandy, but it has certain limitations:

  • it does not watch for file changes
  • it is not integrated into Webpack so it will not flash a message in your browser and terminal like ESLint does in case there is a linting error

In order to have that last piece of the integration puzzle, you need stylelint-webpack-plugin. But that requires either of the two options from above, right? Well, there is another option I came across recently!

Meet react-app-rewired

This package sits between your local app configuration and CRA's config and overwrites CRA on the fly with any loader, plugin, or setting that can be overwritten. So you get all the benefits of CRA without the limitations of "no config". Pretty cool!

Anyway, back to Stylelint.

Let's install what we need first. Optionally you'd want stylelint-config-standard, depending on what kind of stylelint config you want.

yarn add react-app-rewired stylelint stylelint-webpack-plugin --dev

Then create config-overrides.js in your project root.

// config-overrides.js

const StylelintPlugin = require('stylelint-webpack-plugin')

module.exports = {
  webpack: function (config, env) {
    if (env === 'development') {
      config.plugins.push(
        new StylelintPlugin({
            // options here
        })
      )
    }

    return config
  }
  // jest: function(config) {
  //   // customize jest here
  //   return config;
  // },
  // devServer: function(configFunction) {
  //   return function(proxy, host) {
  //     // customize devServer config here
  //     return config;
  //   }
  // }
}

Now in your package.json, "start": "react-scripts start" becomes "start": "react-app-rewired start".

That's it. react-app-rewired will merge this setting with the settings on the CRA end. Your project remains clean and maintainable.

There are also default rewires that you could use, such as relay, eslint (in case you want a different config), etc.

In and out. Ciao!

Integrate Stylelint into Create React App without ejecting

]]>
<![CDATA[Configure Create React App to consume ENV variables during run-time]]>

Or how to speed up the deployments by avoiding building your application for every environment separately

In one of my posts, I described how to Configure Nuxt.js to consume ENV variables during run-time. Nuxt.js is for Vue what Next.js is for React. Both of these frameworks are

]]>
https://vanja.gavric.org/blog/configure-create-react-app-to-consume-env-variables-during-run-time/678621af7de7d4d0ebc3314eMon, 22 Jan 2018 02:03:00 GMTConfigure Create React App to consume ENV variables during run-time

Or how to speed up the deployments by avoiding building your application for every environment separately

In one of my posts, I described how to Configure Nuxt.js to consume ENV variables during run-time. Nuxt.js is for Vue what Next.js is for React. Both of these frameworks are used for server-rendered or statically-exported apps. Because of their isomorphic nature, managing ENV variables is more involved than in a strictly speaking front-end, client-rendered application. Unfortunately, I haven't had a chance to play with Next.js yet, but I got an idea for writing this post from my previous post on How to use Google Analytics with React Router v4 where I am using window._env_ to access my ENV variables instead of more commonly used process.env using Webpack's EnvironmentPlugin. Hope this is useful for others as much as it was for my team and me.

Why do this at all?

If you want to "build once, deploy many", you generally have to re-invent the wheel in JavaScript (front-end) world since the trend seems to be built for each environment separately and "bake" everything inside your bundle(s).
What does the "build once, deploy many" strategy even mean? Imagine that you have your shiny application that you and your team work on and you want to deploy it to many different environments - test, dev, staging, production,... Most likely you will always be running a production build for all of them. Yet, if you are using CRA out of the box none of them are going to result in the exact same bundles. Why? Because Webpack will "bake in" process.env variables depending on the environment where the build was run. This is unnecessary and slow. The approach demonstrated here keeps the entire application build intact and runs a script to generate a separate file that will declare a global window variable (object) that contains all ENV variables specific to that environment only.

Dive right in

It turns out this is also quite easy to accomplish since we will take advantage of the heavy lifting that CRA already does to prepare the variables for the already mentioned EnvironmentPlugin. Keep in mind that CRA uses convention to expose any variable to the client that starts with REACT_APP (ENV and PUBLIC_URL being exceptions).

Writing the script.

// scripts/client-env.js
require('dotenv').config()
const fs = require('fs')
const clientEnv = require('react-scripts/config/env.js')(process.env.PUBLIC_URL || '')

const pathToWrite='./public'
const fileToWrite = `${pathToWrite}/client-env.js`
const globalVarName = '_env_'
const content = `window.${globalVarName} = ${JSON.stringify(clientEnv.raw)}`

try {
  fs.writeFileSync(fileToWrite, content, 'utf8')
} catch (err) {
  // eslint-disable-next-line no-console
  console.log('Error while writing client-env file:', err.message)
  process.exit(1)
}

First we require dotenv so that we have access to our ENV variables through process.env. Require fs for writing the file and then the most important line - declaring clientEnv. Here we require a script from CRA (react-scripts in node_modules directory inside your project) and execute it in the same line. The function that config/env.js file exports takes only one argument (publicUrl). We read this from the current environment and if not defined, pass an empty string.
The rest of it should be self-explanatory.

Depending on your needs, you could allow the script to take command line arguments. Instead of hard-coding the pathtoWrite, fileToWrite, globalVarName, you could do something like this:

/**
 * Remove first two arguments
 * 1st arg: path to nodejs
 * 2nd arg: location of this script
 */
const args = process.argv.slice(2)
const DIR = /DIR=/i
const pathToWrite = args
  .filter(key => DIR.test(key))
  .slice(0, 1)
  .reduce((prev, curr) => prev + curr.replace(DIR, ''), '')

if (!pathToWrite) {
  throw new Error('DIR argument is required.')
}

Plug it in

Now that we have our script, how/when do we run it and how do we consume it? Consuming it is more straight forward so let's address that first.
This file needs to be loaded before the bundle file so head to your public/index.html file and add <script src="%PUBLIC_URL%/client-env.js"></script> just before the closing </body> tag.

Now that we got that out of the way, let's think about where will this need to be executed. For starters, development. So let's add the following line to our package.json file inside "scripts" object:

"build:env": "node scripts/client-env.js DIR='./public'"

Now for development, instead of "start": "react-scripts start", we will have "start": "npm run build:env && react-scripts start" which will generate the file every time we execute npm run start.

For other environments, it depends on your use-case. In most projects we use, there is also a server so we have something like this:
"start": "npm run build:env && NODE_PATH=server-build node server-build/index.js" which generates the file just before the server (re)starts. So your mileage may vary. You could, for example, modify this approach for every client-env.js to be generated centrally in one location for every environment you need it for.

There are, of course, drawbacks to this approach:

  • It is an extra file so your browser may choke sooner if it hits the maximum concurrent connection limit to the same domain
  • Your ENV variables are more exposed. Given that this is a browser, you should not be too worried about this since you should not expose anything sensitive there anyway (if you are, please remove the sensitive info ASAP)
  • Extra configuration. While this reuses the functionality already built-in inside CRA (and just exposes it differently), it is still an extra factor to worry about when it comes to maintainability/portability
  • ENV variables are still going to be baked inside process.env, depending on where the production build was run. If you don't consume that, it should not be a problem (aside from the unnecessary bloat in your bundle file(s)). This can be avoided but requires eject-ing.

We deploy often and having a finished deployment in most cases under 30 seconds makes this approach worth its drawbacks for my team and me.

In and out. Ciao!

Configure Create React App to consume ENV variables during run-time

]]>
<![CDATA[Integrate Google Analytics with React Router v4]]>https://vanja.gavric.org/blog/integrate-google-analytics-with-react-router-v4/678621af7de7d4d0ebc3314dThu, 28 Dec 2017 23:45:00 GMTIntegrate Google Analytics with React Router v4

Or how to accomplish this without polluting your entire application.

If you have to "just integrate that Google Analytics snippet" (can you hear your project manager/team lead/boss / ... already?) into your app and your application is a SPA, you know that it's not that simple.

Luckily, it's not that difficult either 😎

Recommended approaches

The most mature project for integrating Google Analytics into a React project is React-GA. Surely, it is not overly complicated to write an abstraction layer on top of the existing GA API, but why re-invent the wheel when this project is already battle-tested?

React Router v4 changed quite a lot of things since it is a total rewrite. By looking at the React GA's wiki page, we see there are several recommended approaches:

withPageView (HOC)

Initial idea. Use this higher-order component to wrap every page (container component). Every time our container components mount, the page view is tracked. Simple!
There are several drawbacks to this approach, however:

  • wrapping every container component is repetitive
  • separation of concerns (all of a sudden every page is wrapped in this HOC)
  • just does not feel very "React way"

withTracker (HOC)

Another higher-order component, this time to wrap an entire app component or Route component(s) to track page views. Ok, this seems better.
There are again several drawbacks to this approach, however:

  • it requires additional dependency history.
  • since the listener is listening for history changes, this will not fire on the initial page load
  • the listener is hooked into the render cycle which means that the wrapped component - even when it returns false from shouldComponentUpdate - will trigger the GA call

As Redux Middleware

This is better. It catches a router react dispatch of action type @@router/LOCATION_CHANGE and hits GA.
Very clean and succinct. However, not possible to use if you are not using Redux or not letting React Router to dispatch actions.

If none of these approaches seem ideal (like in my case), read on.

Route Component (IMHO best approach)

Let's work backward to see how we want to wire this up. I often find myself working my way "backward" like this when thinking in React.

How to consume it

index.js is the app entry point. Pretty standard React stuff here.

// index.js
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter, Switch, Route } from 'react-router-dom'

import UnauthorizedLayout from 'layouts/UnauthorizedLayout'
import ErrorLayout from 'layouts/ErrorLayout'
import AuthorizedLayout from 'layouts/AuthorizedLayout'
import AuthorizedRoute from 'custom-routes/AuthorizedRoute'
import GA from 'utils/GoogleAnalytics'

import './scss/app.scss'

class App extends Component {
  render () {
    return (
        <BrowserRouter>
          { GA.init() && <GA.RouteTracker /> }
          <Switch>
            <Route path='/auth' component={UnauthorizedLayout} />
            <Route path='/error' component={ErrorLayout} />
            <AuthorizedRoute path='/' component={AuthorizedLayout} />
          </Switch>
        </BrowserRouter>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

Since everything in React Router v4 is a component, our app's root element is BrowserRouter component. There is also usually at least one Switch component that will route to an appropriate route. As an added note, various layouts existence is an abstraction for nested routes with added, well... layout 😉 This also gives me an idea for the next blog post!

We are interested in { GA.init() && <GA.RouteTracker /> }
init will check if the current environment will be using Google Analytics. It will return a boolean and RouteTracker component will be conditionally rendered. Since it is not wrapped in a Switch component, it will be rendered every time depending on what init returns.

Why on Earth would we want to render our GA tracker at all? Well, we want to be able to fire ReactGA's set and pageView methods. This component will always render null since DOM wise we don't need it to do anything. Since it is a React component, we will be able to tap into the component's lifecycle hooks and hopefully have enough context to know when we want to tell GA that the route has changed.
Everything's a component, remember? 😎

Implementation

Now that we defined how we want to consume this, let's get into the implementation.

// utils/GoogleAnalytics.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ReactGA from 'react-ga'
import { Route } from 'react-router-dom'

class GoogleAnalytics extends Component {
  componentDidMount () {
    this.logPageChange(
      this.props.location.pathname,
      this.props.location.search
    )
  }

  componentDidUpdate ({ location: prevLocation }) {
    const { location: { pathname, search } } = this.props
    const isDifferentPathname = pathname !== prevLocation.pathname
    const isDifferentSearch = search !== prevLocation.search

    if (isDifferentPathname || isDifferentSearch) {
      this.logPageChange(pathname, search)
    }
  }

  logPageChange (pathname, search = '') {
    const page = pathname + search
    const { location } = window
    ReactGA.set({
      page,
      location: `${location.origin}${page}`,
      ...this.props.options
    })
    ReactGA.pageview(page)
  }

  render () {
    return null
  }
}

GoogleAnalytics.propTypes = {
  location: PropTypes.shape({
    pathname: PropTypes.string,
    search: PropTypes.string
  }).isRequired,
  options: PropTypes.object
}

const RouteTracker = () =>
  <Route component={GoogleAnalytics} />

const init = (options = {}) => {
  const env = window._env_ || {}
  const isGAEnabled = !!env.REACT_APP_GA_TRACKING_ID

  if (isGAEnabled) {
    ReactGA.initialize(
      env.REACT_APP_GA_TRACKING_ID, {
        debug: env.REACT_APP_GA_DEBUG === 'true',
        ...options
      }
    )
  }

  return isGAEnabled
}

export default {
  GoogleAnalytics,
  RouteTracker,
  init
}

init is fairly easy. We execute ReactGA.initialize if the current ENV is configured to use GA. Why window._env_ and not process.env? Another blog post idea!
EDIT: If you are more interested in this, here's the post Configure Create React App to consume ENV variables during run-time

GoogleAnalytics is our component in charge of using ReactGA when the route changes. How do you ask? Well, we know that our component will be nested inside BrowserRouter (or any other React Router v4 router). If the idea for this component is to be "rendered" on every page change, then we create a route that matches everything. That is exactly what RouteTracker does.

Slowly peeling the layers, we finally get to the GoogleAnalytics component! I mentioned that we will take advantage of the React component's lifecycle hooks.
So componentDidMount will conveniently fire our logPageChange method when it mounts or - more specifically - on the initial page load.
Another hook we tap into is componentDidUpdate. Each time this component updates (for whatever reason, but most likely for a route change), we compare previous props (provided to us through a parameter) with current props. If the pathname and/or query string is different, we log the page change again.
The logPageChange method itself uses React GA's method set and pageView to actually trigger the page change on the Google Analytics side. We also include location field with window.location.origin included due to ReactGA's issue.

In and out. Ciao!

Integrate Google Analytics with React Router v4

]]>
<![CDATA[How to perform force refresh in iOS 11 Safari]]>

Or how to perform real-device testing without having to clear the cache globally.

Lately, I found myself testing on real devices often and noticed that the cache in iOS 11 is quite stubborn. In previous iOS iterations, hitting the reload button would force refresh. Not in iOS 11.
I was

]]>
https://vanja.gavric.org/blog/how-to-perform-force-refresh-in-ios-11-safari/678621af7de7d4d0ebc3314bMon, 18 Dec 2017 05:41:00 GMTHow to perform force refresh in iOS 11 Safari

Or how to perform real-device testing without having to clear the cache globally.

Lately, I found myself testing on real devices often and noticed that the cache in iOS 11 is quite stubborn. In previous iOS iterations, hitting the reload button would force refresh. Not in iOS 11.
I was clearing the cache through Settings (Settings -> Safari -> Clear History and Website Data), but that got old pretty quickly. So I decided to ask my good friend Google. Unfortunately, Google's response was pretty much ¯\_(ツ)_/¯

I did discover by accident that holding the reload button will show a dialog at the bottom with two options, one of them being "Reload Without Content Blockers".

Here it is in action:
How to perform force refresh in iOS 11 Safari

In and out. Ciao!

How to perform force refresh in iOS 11 Safari

]]>
<![CDATA[Check if an array contains numbers succinctly]]>

Or how to write a one-liner and still keep your code expressive.

Here's that one-liner.

const containsNumbers = !someArr.some(isNaN)

What's going on here?
First, our array uses the some method. This method loops over the array and accepts a function that tests if at least one element passes the

]]>
https://vanja.gavric.org/blog/check-if-an-array-contains-numbers-succinctly/678621af7de7d4d0ebc3314aMon, 11 Dec 2017 05:25:00 GMTCheck if an array contains numbers succinctly

Or how to write a one-liner and still keep your code expressive.

Here's that one-liner.

const containsNumbers = !someArr.some(isNaN)

What's going on here?
First, our array uses the some method. This method loops over the array and accepts a function that tests if at least one element passes the criteria set by that function.
Notice "at least one" element part. Let's say you used the forEach or filter method instead of some. Now let's pretend that you are looping over a large array but you are only interested in the first occurrence of a number. Why take the performance hit and loop over an entire array, right? So you would have to manually exit the loop based on a condition. Not with some, it is handled for you.

The second part is a callback function. We can rely on the global (window) isNaN function here. Keep in mind that there is also Number.isNaN method that provides more predictable results when it comes to type coercion. Use whatever is more appropriate in your case, but note that the global isNaN function has better browser support.

In and out. Ciao!
Check if an array contains numbers succinctly

]]>
<![CDATA[How to auto-format your code effortlessly]]>

Or how to be lazy and still win.

Is Prettier really prettier?

How do you format your code - JS, CSS, HTML,...? And before you answer "manually", please don't do that. Because life is too short :)
Prettier then, right? Everywhere I turn - blog posts, meetups, colleagues, and

]]>
https://vanja.gavric.org/blog/how-to-auto-format-your-code-the-right/678621af7de7d4d0ebc33149Mon, 04 Dec 2017 03:53:00 GMTHow to auto-format your code effortlessly

Or how to be lazy and still win.

Is Prettier really prettier?

How do you format your code - JS, CSS, HTML,...? And before you answer "manually", please don't do that. Because life is too short :)
Prettier then, right? Everywhere I turn - blog posts, meetups, colleagues, and friends, it seems that Prettier is getting all the attention and hype.
By its description, Prettier is an Opinionated Code Formatter. Here's the thing though. You and your team have probably already defined your coding style. And if you work on multiple projects at once, that coding style may not be the same. So why would you want to use an auto-formatting tool that:

  • is opinionated and mostly likely going against your coding style
  • requires separate configuration to reconcile that difference
  • does not have project specific rc configuration
  • has weird defaults (double quotes in JavaScript - really?)
  • has limited configurability

Your coding style is most likely already defined in the form of an rc file.

Have your auto-formatter work with your code base, not against it!

In defense of Prettier, it seems that the goal is to avoid building and enforcing a style guide by simply letting Prettier format it with its defaults. Hummmm...

EDIT: It seems that Prettier can integrate with eslint. Not sure if I missed this, or if it was added later. This helps, but did not change my opinion as a whole. Even with this, there is a mention of conflicting rules between Prettier and ESLint. Plus, it still requires configuration. Should your project be aware of an auto-formatter? No, thanks.

Ok, so no Prettier. What then?

A tool that can read your existing configuration organically without needing a separate configuration.

NOTE: Using this approach assumes you are linting your code already. If you are not, you should seriously consider doing so. Not only does it enforce a consistent coding style, but static code analysis can also prevent bugs.
To get started with JavaScript, check out ESLint. For CSS, my recommendation is Stylelint.

Javascript

Meet eslint-formatter. This is a plugin for Sublime Text 3 that uses your .eslintrc file to auto-format your code, but only in the context of that particular project.
The only thing configuration you may need to do is to enable the format on the save option.
If your markup is in Javascript (JSX, Vue,...) and you are linting it with a correct plugin/extend, eslint-formatter will simply pick it up.

(S)CSS/PostCSS/Less

Chances are that you are linting your Javascript, but are you linting your style? If you are not, meet Stylelint.
When you are done configuring, a tool called Stylefmt will read stylelint config and format your style. Easy-peasy!
Here are some of the projects for your favorite text editor:

Job done! Lazy and right 😎

How to auto-format your code effortlessly

In and out. Ciao!

]]>
<![CDATA[Configure Nuxt.js to consume ENV variables during run-time]]>

Or how to go against the well-established patterns and still win.

In my team, we started re-writing the platform that was long in the tooth with a lot of technical debt. It is still serving 50+ million unique viewers, but it well deserves to be retired.
Performance is naturally the

]]>
https://vanja.gavric.org/blog/configure-nuxt-js-to-consume-env-variables-during-run-time/678621af7de7d4d0ebc33148Sun, 26 Nov 2017 21:42:00 GMTConfigure Nuxt.js to consume ENV variables during run-time

Or how to go against the well-established patterns and still win.

In my team, we started re-writing the platform that was long in the tooth with a lot of technical debt. It is still serving 50+ million unique viewers, but it well deserves to be retired.
Performance is naturally the biggest concern for our new baby as well as SEO.
So we knew we could not rely on traditional SPA patterns. We needed server-side rendering and we decided to go with Nuxt.js.

Nuxt.js

Nuxt.js is an opinionated framework for creating universal Vue.js applications in a way that abstracts client/server distribution. It can also be used as a static site generator. It can be wired, for example, to listen for changes from an API and (re-)generate the content making sure it always serves static content for lightning-fast performance. Pretty awesome if you ask me.
But this is not something super new. What makes Nuxt.js interesting is the ability to allow developers to spin up an SSR project quickly and start delivering features right away.
Having said that, convention over configuration is rarely something I choose to go with unless there is a really, really good reason. Swimming against an opinionated framework is rarely something that turns out well.
That's where we will go in this post. And it will turn out fine. I promise!

We want to "build once, deploy many"

Our cloud architect approaches me and explains the problem. Every time we change code, the codebase needs to be built specifically for every environment separately. This is often slow. What is the common difference in code across different environments? Environment variables! Although what he asked for makes sense, my initial reaction was:
Configure Nuxt.js to consume ENV variables during run-time
You want what?
But we have all this artillery of compilers, transpilers, pre- and post-processors at our disposal that take days to set up and that build the code for every environment! Plus, there are so many potential problems for leaking sensitive data to the client.
Build once, deploy many makes perfect sense in the world of software development, however. But not necessarily in the Javascript world...

Let's do it!

So I started my research and Google was certainly not my friend. The cynic in me asked, "Perhaps for a reason?" 👿
There was some chatter, but no solution. That meant that I had to come up with a solution on my own instead of copy-pasting code (that's what we as developers do all the time anyway, right?) 😜

Understanding how Nuxt.js does it

Nuxt.js has a file nuxt.config.js. This file is used when instantiating Nuxt.js programmatically:

const config = require('nuxt.config.js')
const nuxt = new Nuxt(config)

In this file, it is possible to define env object of ENV variables. It is a common practice to require config dynamically here. This gets passed to webpack's definePlugin and can be used for client and server-side like so: process.env.propertyName or context.env.propertyName. These variables are then baked into Nuxt.js' build (see .nuxt/utils.js -> getContext). For more info, see the official Nuxt.js env page.

Notice the mention of webpack? Yup, that means compilation and... that this is not going to work for us.

Hack away!

Understanding how Nuxt.js works means that:

  • we can't use env inside nuxt.config.js anymore
  • any other dynamic variables (like inside head.meta) need to be passed to nuxt.config.js object during run-time

The code in server/index.js:

// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')

becomes

// Import extended Nuxt.js options
const config = require('./utils/extendedNuxtConfig.js').default

where utils/extendedNuxtConfig.js is:

import config from 'config'
import get from 'lodash/get'

// Import and Set Nuxt.js options
const defaultConfig = require('../../nuxt.config.js')

// Place extended config here
const extendedConfig = {}

// (Shallow) extend Nuxt.js config here
const nuxtConfig = {
  ...defaultConfig,
  ...extendedConfig
}

// Do final object manipulation for things where
// extending objects is not appropriate
if (get(nuxtConfig, 'head.meta')) {
  nuxtConfig.head.meta.push({
    hid: 'og:url',
    property: 'og:url',
    content: config.get('app.canonical_domain')
  })
}

export default nuxtConfig

The big elephant in the room

Ok, this solves a minor problem of having dynamic ENV data outside the scope of env in nuxt.config.js. The original question remains, however.

I initially thought I would have an abstraction like sharedEnv.js that would be used for:

  • client: creating env.js file that would be loaded on the client in global window scope (window.env.envKey)
  • server: imported in files where needed (the most straightforward part)
  • isomorphic code: something like context.isClient ? window.env[key] : global.sharedEnv[key] (and write a small utility behind it)

This seems very hacky and just not that great. This abstraction does take care of one of my biggest concerns which was leaking sensitive data to the client. Just like the env object from nuxt.config.js, this forces everyone on the team to consciously add a value so nothing gets leaked. Not intentionally at least.

Vuex to the rescue

While fiddling with the window scope, I realized that the Vuex store is exported to the global window scope. This decision was most likely made because of Nuxt.js' isomorphic nature. Vuex is a Flux-inspired data store specifically tailored for Vue.js apps.

So why not use Vuex for our shared env variables?
It is certainly more organic than the previous approach and this data is in a way global state so it fits the bills perfectly.

We start with our good friend server/utils/sharedEnv.js

import config from 'config'

/**
 * Set up the object that will be available to both the server and the client.
 * For the sake of simplicity, please keep this object flat.
 * Be extra diligent about not leaking any sensitive info here!!
 *
 * @type       {Object}
 */
const sharedEnv = {
  // ...
  canonicalDomain: config.get('app.canonical_domain'),
}

export default sharedEnv

This is executed during the server start-up process. We then add it to our Vuex store.

/**
 * Gets the shared environment.
 * Documentation suggests it is only executed server side, but not wrapping
 * this in a conditional results in an error.
 * https://nuxtjs.org/guide/vuex-store/#the-nuxtserverinit-action
 *
 * @return     {Object}  Shared environment variables.
 */
const getSharedEnv = () =>
  process.server
    ? require('~/server/utils/sharedEnv').default || {}
    : {}

// ...

export const state = () => ({
  // ...
  sharedEnv: {}
})

export const mutations = {
  // ...
  setSharedEnv (state, content) {
    state.sharedEnv = content
  }
}

export const actions = {
  nuxtServerInit ({ commit }) {
    if (process.server) {
      commit('setSharedEnv', getSharedEnv())
    }
  }
}

We take advantage of the fact that nuxtServerInit runs on... uhm... server init. It is possible to save our shared env data. Notice the complication here: getSharedEnv method which then checks if this is run on server again. This is necessary to ensure that the client does not run into an error and tries to execute require.

Consume it!

Now that we have our shared env variables in our store, we can consume it from our components like so:
this.$store.state.sharedEnv.canonicalDomain

Win!

Not so fast. What about plugins?

Certain plugins may need environment variables to be configured when letting know Vue.js we want to use: Vue.use(MyPlugin, { someEnvOption: 'no access to vuex store here' })

We ran into a racing condition here. Vue.js is trying to initialize itself before Nuxt.js registers our sharedEnv object to the Vuex store.
While the exported function that registers a Vue.js plugin provides us with a context object (from which we have access to the store), sharedEnv is still empty.

The answer to this is making the plugin function async and await a manual nuxtServerInit dispatch. This ensures that Vue.js waits until we have our sharedEnv object available.

Here's the code:


import Vue from 'vue'
import MyPlugin from 'my-plugin'

/**
 * Adds configured MyPlugin asynchronously.
 */
export default async (context) => {
  // perform a store action manually to have access to `sharedEnv` object
  await context.store.dispatch('nuxtServerInit', context)

  const env = { ...context.store.state.sharedEnv }

  Vue.use(MyPlugin, { option: env.someKey })
}

Now it is a win!

In and out. Ciao!

Configure Nuxt.js to consume ENV variables during run-time

]]>
<![CDATA[Welcome to my blog!]]>

After several years of postponing the "project blog", I finally decided to do it!

Back in my college days, for about 60-70% of all my courses, I was a TA and loved it! Then later in my career, on-boarding new developers was one of the most pleasurable aspects

]]>
https://vanja.gavric.org/blog/welcome/678621af7de7d4d0ebc33147Sun, 26 Nov 2017 00:26:00 GMTWelcome to my blog!

After several years of postponing the "project blog", I finally decided to do it!

Back in my college days, for about 60-70% of all my courses, I was a TA and loved it! Then later in my career, on-boarding new developers was one of the most pleasurable aspects of my job. Guess I love to teach!
I also had to change two jobs this year so I experienced quite a bit of Impostor syndrome and this blog is going to be my therapy now 😎
Writing is not easy, but if it wasn't easy, it wouldn't be a challenge, would it?

Writing is nature's way of telling us how lousy our thinking is.
Leslie Lamport

What is this going to be about?

This blog is going to be mostly tech-related. Primarily web development with lots of Javascript. Until your head explodes. Did I mention I love Javascript?
I might sneak in a few smartphone and travel/photography-related posts as well.
The form will be short and sweet, although there could be some tutorials and more in-depth posts.

How often will this Interwebs torture be happening?

My goal is to write weekly. It will be easy in the beginning since I have a lot of notes about what I want to write. My daytime job challenges will also provide me with plenty of stuff to write about.

Who are you anyway?

I am a full-stack web developer, primarily in Javascript with a focus on front-end development. I've been tinkering with computers since my elementary school days. And before that, I was busy begging my parents to get one and reading about them. How many years of experience that is? Enough to be somewhat conservative with tech and not follow/use every single new and shiny thing that is out there 😱
I have lived in Europe, and Canada, and I currently reside in the United States. Love photography and naturally, travel. Currently, I work as a Senior Front-End Software Engineer at SheKnows Media.
Many projects and two degrees in information technology later I am here - destined to write this blog 😂 [drum roll.mp3 here]

What's with the photos?

I love photography. Now that I am writing a blog I now have some practical use for all those photos I have taken throughout the years 😉 So every post will be accompanied by a photo taken straight out of my camera.

Why should I follow your blog?

Because!😎
Seriously, you shouldn't. This is probably going to be torture, and I would not recommend it.
But seriously, seriously now, this blog is all about giving back to the development community. Without you guys out there, your blogs and FOSS projects, I probably would not have my career now. This blog is my way of joining you guys in the hope that somebody will learn something new from my ramblings here. Pay it forward!

In and out. Ciao!

Welcome to my blog!

]]>