Wednesday, April 28, 2021

Dark mode for Next.js statically-generated apps using styled-components (and good UX)

In this post I will explain one way to implement dark mode for a Next.js Statically-generated application (SSG) using styled-components.

Good dark mode should:

  • Adapt to user's system color mode.
  • Give the ability to switch between color modes, and remember user's preference
  • Avoid flash of wrong colors (The situation where the page displays light mode on load, then immediately switches to dark mode, or vice versa).

This post is using Next.js 10.0 and styled-components 5.2


we will start by adding some CSS custom properties (aka CSS variables) to the document body so that we can use it from anywhere in the app.

To do that, we need to override the default app component that Next.js uses to initialize the application.

Following the instructions here, create a ./pages/_app.js file like this one.

import { createGlobalStyle } from "styled-components";
import { COLORS } from "../constants";

const GlobalStyle = createGlobalStyle`
  body {
    --bg-color: ${COLORS.light.background};
    --text-color: ${COLORS.light.text};
    color-scheme: light;

    background-color: var(--bg-color);
    color: var(--text-color);
  }

  body.dark {
    --bg-color: ${COLORS.dark.background};
    --text-color: ${COLORS.dark.text};
    color-scheme: dark;
  }
`;

function MyApp({ Component, pageProps }) {
  return (
    <>
      <GlobalStyle />
      <Component {...pageProps} />
    </>
  );
}

export default MyApp;

We added a global style with some CSS variables attached to the body, they have light mode colors by default and we override them with the .dark class.

Of course , you can add any variables you would like to use in your app and reference them in other components.

color-scheme is just a hint to the browser to use the corresponding color mode for user-agent controls, but you don't have to use it.

Some people prefer adding custom properties to the :root element. In practice, that doesn't really matter that much. I found it easier in this case, because when using JavaScript (as you'll see in a moment), we get to set the body class name instead of setting each individual variable.
You can still add them to :root and override them in the styles of the body element or other elements through the app if you prefer. this is a good article about it.

Now we can use these variables in any styled component (or any CSS, for that matter) in the app. Imagine we have a paragraph that changes color when color mode changes, it would look something like this.

import styled from "styled-components";

export default styled.p`
    color: var(--text-color);
`;

This is probably a bad example because color is an inherited property, but you get the idea :D

Great! Now we have some components in our app that are ready to adapt to color modes.

Adapting to system color mode

Now let's make it respect the user's system color mode. To do so, we need to use the prefers-color-scheme media query.

We could do something like this in our styles

@media (prefers-color-scheme: dark) {
  --bg-color: ${COLORS.dark.background};
  --text-color: ${COLORS.dark.text};
}

But the problem is that we want to store user's preference, so if they prefer light mode while their device is on dark mode, they will see a flash of dark mode in this case when the page opens (and vice versa), so this media query here won't help us with our end goal.

We will move that check to JavaScript instead.

Let's also create a React context while we're at it, so that we can share and update the color mode from anywhere in the app.

import {
  createContext,
  useContext,
  useEffect,
  useState,
} from "react";

const ColorModeContext = createContext();

// Called when the app first mounts
// to decide which state the context should have initially
function getInitialColorMode() {
  if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
    return "dark";
  }

  return "light";
}

function activateColorMode(colorMode) {
  if (colorMode === "dark") {
    document.body.classList.add("dark");
  } else {
    document.body.classList.remove("dark");
  }
}

function ColorModeProvider({ children }) {
  const [colorMode, setColorMode] = useState(null);

 // `window` doesn't exist while rendering on the server
 // So we defer the decision on the initial color mode to when the component actually runs in the browser.
 // This also prevents mismatches between server-side and client-side renders
  useEffect(() => {
    setColorMode(getInitialColorMode());
  }, []);

  useEffect(() => {
    if (!colorMode) {
      return;
    }
    
    activateColorMode(colorMode);
  }, [colorMode]);

  return (
    <ColorModeContext.Provider value={{ colorMode, setColorMode }}>
      {children}
    </ColorModeContext.Provider>
  );
}

function useColorMode() {
  const context = useContext(ColorModeContext);

  if (!context) {
    throw new Error("Must be inside colorModeContext provider");
  }

  const { colorMode, setColorMode } = context;

  return { colorMode, setColorMode };
}

export {
  ColorModeProvider,
  useColorMode,
  getInitialColorMode,
  activateColorMode,
};

Please note that React will complain if the rendered content on the server didn't match the content on the client when rehydrated. And this is the reason why we set the color mode in a useEffect, so it'll only run client-side, deferring that decision until the second render, making React pass the first render successfully.

You might want to optimize ColorModeContext.Provider's value prop, depending on your application. Check the official React docs for more on that.

Now let's wrap our app component into this context. So in the custom pages/_app.js

function MyApp({ Component, pageProps }) {
  return (
    <>
      <GlobalStyle />
      <ColorModeProvider>
        <Component {...pageProps} />
      </ColorModeProvider>
    </>
  );
}


Switching between color modes

Now we can have a button somewhere to toggle between light and dark mode. Something like this.

import { useColorMode } from "../colorModeContext";
import { COLOR_MODES } from "../constants";

function ColorModeButton() {
  const { colorMode, setColorMode } = useColorMode();
  const isDarkMode = colorMode === COLOR_MODES.dark;
  
  <button
     onClick={() =>
       setColorMode(isDarkMode ? COLOR_MODES.light : COLOR_MODES.dark)
     }
  >
     {isDarkMode ? (
       "Turn on the lights"
    ) : (
      "Turn off the lights"
    )}
  </button>
}

export default ColorModeButton;

Now when the button is clicked, the color mode is switched. Cool stuff!


Saving user's preference

The missing piece is saving the user's choice so on their next visit they will see the color mode they've last chosen. Local storage to the rescue..

Let's modify our getInitialColorMode function in the color mode context we created earlier to also check local storage for a key we will call prefers-dark, and if found, it takes priority over system color mode.

function getInitialColorMode() {
  if (window.localStorage.getItem("prefers-dark")) {
    return window.localStorage.getItem("prefers-dark") === "true"
      ? "dark"
      : "light";
  }

  if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
    return "dark";
  }

  return "light";
}

Now to set it, we will modify the useColorMode function in the same file.

function useColorMode() {
  const context = useContext(ColorModeContext);

  if (!context) {
    throw new Error("Must be inside colorModeContext provider");
  }

  const { colorMode, setColorMode } = context;

  // A new function to save user's preference to local storage
  const setColorModePersisted = useCallback((newColorMode) => {
    setColorMode(newColorMode);
    window.localStorage.setItem(
      "prefers-dark",
      newColorMode === "dark"
    );
  }, []);

  return { colorMode, setColorMode: setColorModePersisted };
}

Let's recap what we have until now.

  • body has some CSS variables for light mode and dark mode.
  • color mode is stored in a React context and shared throughout the app.
  • A component can change the color mode by setting it in the context, an effect fires whenever color mode changes and adds or removes the dark class from the body, overriding the CSS variables.
  • Initial color mode in context is set on app mount with a useEffect, by default, it's set to the system color mode, but if the user has changed the color with the toggle button, we will remember it and use it in the next time they open the page.

So far so good, the only problem now is that our pages are pre-rendered on the server, and the server doesn't have a way to know on which mode the page should initially render.

The only way to know is to look into local storage and then otherwise test the media query at runtime, but because the page is pre-rendered and defaults to a certain (light in our case) mode, the browser renders the html it received from the server until react starts rehydrating and useEffects are run to set the correct color mode.

This will cause the flash of light mode that we talked about in the beginning.


Avoiding flash of wrong colors

We need to do these checks (local storage and media query) through JavaScript, but we want it to run immediately before any content is rendered, so could we set the CSS variables early, and therefore render in the correct mode.

This can be solved by injecting a script tag that runs before anything else to set these variables.

The way it's done in Next.js is by creating a custom document. Create a ./pages/_document.js file like this one.

import Document, { Html, Head, Main, NextScript } from "next/document";
import {
  getInitialColorMode,
  activateColorMode,
} from "../contexts/colorModeContext";

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    ...// unrelated styled-components initialization code
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <script
            dangerouslySetInnerHTML={{
              __html: `(() => {
                  ${getInitialColorMode.toString()}
                  ${activateColorMode.toString()}
                  activateColorMode(getInitialColorMode())
                  })()`,
            }}
          ></script>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Here a script tag is injected right after the body's opening tag, so we are sure that the browser will stop and run it before it sees any other html in the body.

React doesn't let you render arbitrary script tags for security reasons, so we have to use the dangerouslySetInnerHTML prop, but don't worry, it's safe in that case as we're using our own code, not user input, for example.

We make use of JavaScript's toString function of the Function prototype, which serializes a function definition to a string. This way we can reuse the code we've written in the other file without having to copy/paste it.

I find that this works well and looks nice for a simple script like this. You might prefer to copy/paste the code inside the __html string and it's also fine, but the downside is that you'll lose reusability and code editor help as you'll have to use only strings.

Keep in mind that this code runs in isolation, outside any of our React/Webpack code, so we can't import/export, use variables from the outside scope, etc.

Finally we're wrapping everything inside an IIFE to keep the global scope clean.

And we're done! 🎉 The browser will render the page in the correct color mode on the first paint, then react kicks in and sets the initial color mode for the context using the same function. The user can change the color mode and it will persist and be used the next time they open the app.

One last thing, with our current setup, in our toggle component you would see a flash of wrong state. That's because it depends on the state from the context, which is not known until react runs in the browser and useEffect sets the correct mode.

So until that happens, our button might show "Turn off the lights" while it's dark mode.

We can get around this by only rendering the button if we know the color mode. It will defer rendering the button until the color mode is set but it's a better user experience (the button won't be interactive until then anyway).

import { useColorMode } from "../contexts/colorModeContext";
import { COLOR_MODES } from "../constants";

function ColorModeButton() {
  const { colorMode, setColorMode } = useColorMode();
  const isDarkMode = colorMode === COLOR_MODES.dark;
  
  if(!colorMode) {
    return null;
  }
  
  return (
    <button
      onClick={() =>
         setColorMode(isDarkMode ? COLOR_MODES.light : COLOR_MODES.dark)
         }
   >
      {isDarkMode ? (
         "Turn on the lights"
      ) : (
        "Turn off the lights"
      )}
      </button>
  )
}

export default ColorModeButton;

And that's all I got.


Code samples were mostly copied from a simple open source project I created for my short stories, arc. You can see the code here, and you can also see it in action here (and maybe enjoy a story or two along the way as well!).

I hope you found it useful. And as always, leave a comment with your thoughts.

Tuesday, June 16, 2020

React - setInterval based on prop change with the useEffect hook

There are some times where you would like to setInterval and then clear it after a prop changes, say for example you want to display a different status/welcome message every second while you load some data (Postman/Discord-style).

So you want to set the interval whenever the data starts loading and clear it when it's done (to avoid memory leaks and unnecessary re-renders).

Let's go straight for the code, this is how to do it.

useEffect(() => {
  if (isLoading) {
    const interval = setInterval(() => {
      setGreeting(getNextGreeting());
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  }
}, [isLoading]);

To see why this works we need to think a little backwards about how useEffect runs your code.

On the first render, this part executes, setting the interval and keeping a reference to the interval instance that's specific to this useEffect call.

useEffect(() => {
  if (isLoading) { // <-- true
    const interval = setInterval(() => {
      setGreeting(getNextGreeting());
    }, 1000);

    /*
    return () => {
      clearInterval(interval);
    };
    */
  }
}, [isLoading]);

When our data is here and ready, the isLoading prop (or state, for that matter) becomes false, and when that happens, this part is executed first, clearing the current interval instance.

useEffect(() => {
  /*
  if (isLoading) {
    const interval = setInterval(() => {
      setGreeting(getNextGreeting());
    }, 1000);
  */
    //return () => {
      clearInterval(interval);
    //};
  }
}, [isLoading]);

Then on the next render, the effect is run with the new value for isLoading, which will be false in our case

useEffect(() => {
  if (isLoading) { // <-- false
    /*
    const interval = setInterval(() => {
      setGreeting(getNextGreeting());
    }, 1000);

    return () => {
      clearInterval(interval);
    };
    */
  }
}, [isLoading]);

By wrapping the whole effect in a condition, we prevent setting a new interval when the condition is not met.

To sum it up, here's what happens

  • isLoading is true, the effect runs, sets interval.
  • isLoading gets updated to false, effect clears up the current interval first then goes for another run.
  • Condition is not met, don't set a new interval.

Also note that the effect listens only for changes in the isLoading prop/state, which is also a boolean (primitive value, can be safely shallow-compared for changes).

It can get a little tricky sometimes when you try to play around with React and the browser's imperative APIs. I hope this was clear enough.

As always, ask questions and leave notes, all is welcome.

Wednesday, January 9, 2019

You might have zombie React components lurking around your app


import React from "react";

class Cat extends React.Component {
    render() {
        if (this.props.cat.color === COLORS.BLACK) {
            return null;
        }
        return <p> {`Colorful ${this.props.cat.name}!`} </p>;
    }
}

What does this snippet return when used as a React component inside an app?

Well if the cat is of a black color, you won't see anything, not even an empty DOM node. But if you open your React devtools and peek at it, you're going to see this

<Cat/>

This is a full grown react component, it has its own copy of props, maintains its own state and responds to lifecycle methods. Worse, this component can cause crashes if you return null because the absence of a prop does not make sense in render, and consequently doesn't make sense in other lifecycle methods. This means if you don't pay attention to every lifecycle method, your app is gonna go boom. I've see that happen.

This is a waste of memory and CPU cycles and doesn't offer value for your app.

This doesn't mean you shouldn't ever do this. Components like this can make much sense in certain contexts. Rather, what I'm saying here is to be aware of the fact that they are still components in the React component hierarchy even though they are not really rendered.


The takeaways

  • Pay attention for dead React components in your app.
  • Whenever you don't want a component to render under a certain condition, always prefer checking for that condition in its parent.
  • When iterating over an list of data to map it to components, it's even better to filter the data before sending it to the component.
  • If you find yourself in a situation where you have to do this, prefer functional components over class components.

Monday, June 11, 2018

Why Javascript objects are powerful


This post discusses some features and uses for Javascript objects that make them one of the most powerful features of the language.


Named parameters

Named parameters is a very neat feature that a lot of newer languages has and it's indeed more readable than position-based parameter list.

Javascript has no direct support for named parameters, but luckily the object destructuring syntax can come in handy here; here's an example

function say({message, shouldScream}) {
    return shouldScream? message.toUpperCase(): message;
}

// Usage
say({message: "cats are awesome", shouldScream: true});

This is much more readable than

say("cats are awesome", true);

and with more than two parameters, it gets really more obvious.

With this, it will be way easier to leave some of the arguments out without having to fill in the gaps with undefineds and also it will be possible re-order them because you're just passing objects around.

Also you can add defaults to them

function say({message, shouldScream = true}) {
    return shouldScream? message.toUpperCase(): message;
}

// Same result
say({message: "cats are awesome"});

These default values can also be used to validate the input by assigning them to functions that throw when the value is missing or not valid.


Easy to create

Objects in Javascript can be created just out of nowhere, with the object literal {} or Object.create method.


Easy to compose

Objects in Javascript are really easy to compose into new objects with compound capabilities

const canFly = {canFly: true};
const canWalk = {canWalk: true};

const chicken = Object.assign({name: "Ken Chic"}, canWalk);
const Pigeon = Object.assign({name: "Eon Pig"}, canFly, canWalk);


Return multiple values

Objects can be used with a function's return statement to return multiple values

function updateCat({catId, props}) {
    // update cat in database...
    return {
        isUpserted: updatedCat.isUpserted,
        updatedCat: updatedCat.result
    }
}


Can be used as other data structures

Objects in Javascript can be used as hash maps or enums for fast lookup times. Their syntax make them really natural to read and write for these use cases.

const config = {MAX_LENGTH: 200};

You can make them a little more faster by creating objects without a prototype so there's no time spent traversing the prototype chain if the value is not found.

const config = Object.create(null);
config.MAX_LENGTH = 200;

Although most of the time you won't need this optimization, and we now have the Map objects designed specifically for this.

You can also loop through them with the handy for .. in statement.


Makes it easy to extract the props you need

When you need to extract a subset of the properties of an object, again the destructuring syntax makes it a lot easier to do that.

const {name, age, funny} = person;

If the property is not in the object, it will have an undefined value, you can add default values to make sure they will always have a value.

const {name, age, funny = true} = person;


That's it. If you have other ideas please share them in the comments.