Scaling / Resizing Responsive Retina CSS Sprite Icon in React

Images are crucial elements that will bring a strong visual power to a web page. Too many images, however, will deteriorate the performance of a web page due to too many requests to the server.

One of the optimisation technique we can use is CSS Sprite, which basically combining multiple images into one image. Then, you could use CSS background-position to shift around and only display part of it.

Pros of CSS Sprite

  • Reduce the requests to server and allow more parallelism request for other resources.
  • Reduce overhead of handshake and header for each HTTP requests.

Cons of CSS Sprite

  • Too large image files may slow down First Meaningful Paint metric and negatively impact other metrics as well. Always benchmark and measure!
  • May have some overhead to regenerate the image sprites every time you add a new image.

Creating CSS Sprite Icon

Today, we are going to try to create an Icon component using CSS sprite in React and see how it works. You can clone the starter project and follow the exercise.

Creating CSS Sprite

There are many ways to generate a CSS sprite:

  • Using cloud tools such as Toptal Sprite Generator and etc.
  • Using image tool such as ImageMagick, Adobe Illustrator and etc.
  • Using Grunt / Gulp / Node tools such as sprity.

For this post, we are not going to look into how to generate the sprite, this process will have integrate into your development flow such as your CI/CD flow. For this tutorial, we already have some generated image sprites that is ready to use in the public/images which contains some icons of social network:

  • icon.webp
  • icon@2x.webp
  • icon@3x.webp.

These icons are generate from image get from flaticon.com.

Let’s get started by adding some classes in App.css:

// Other CSS

+.icon {
+  background: url(/images/icon.webp) no-repeat top left;
+  width: 32px;
+  height: 32px;
+}
+
+@media only screen and (-webkit-min-device-pixel-ratio: 2) {
+  .icon {
+    background: url(/images/icon@2x.webp) no-repeat top left;
+    background-size: 32px 128px;
+  }
+}
+
+@media only screen and (-webkit-min-device-pixel-ratio: 3) {
+  .icon {
+    background: url(/images/icon@3x.webp) no-repeat top left;
+    background-size: 32px 128px;
+  }
+}

We added icon class and set the background to the image with width and height of 32px. Also, to make it display nicely in Retina display devices, we also added 2x and 3x images using -webkit-min-device-pixel-ratio media queries.

Next, we will create an Icon component to display the image. Let’s create a src/Icon.tsx:

import { useMemo } from "react";

export type IconType = 'facebook' | 'instagram' | 'twitter' | 'whatsapp';

export interface IconProps {
  iconName: IconType;
  size?: number;
}

const iconIndex: Record<IconType, number> = {
  facebook: 0,
  instagram: 1,
  twitter: 2,
  whatsapp: 3,
}
const iconSize = 32;

export const Icon = ({ iconName, size = iconSize }: IconProps) => {
  const positionStyle = useMemo(() => {
    return {
      backgroundPosition: `-0px -${iconIndex[iconName] * iconSize}px`,
      transform: `scale(${size / iconSize})`,
    }
  }, [iconName, size]);

  return (
    <div className="icon" style={positionStyle} />
  )
}

By using iconIndex and iconSize, we can easily calculate the position of each image that we want to display. We will pass the iconName as the props to the Icon component and reuse the same component anywhere we want to.

In order to scale the icon, we have added a size props to allow us to change the size of the icon by calculating the scale ratio using the desire size against the actual icon size.

Lastly, let’s add some code to test our Icon component in App.tsx:

import './App.css';
+import { Icon } from './Icon';

function App() {
  return (
    <div className='gallery'>
+      <Icon iconName="facebook" size={16} />
+      <Icon iconName="instagram" size={24} />
+      <Icon iconName="twitter" size={32} />
+      <Icon iconName="whatsapp" size={40} />
    </div>
  );
}

export default App;

Great! We have successfully added 4 Icons in the page with different size and iconName and let’s run yarn start to see the result.

Now we should be able to see the 4 icons is render properly with different sizes.

Conclusion

CSS Sprite is one of the way to optimise your image assets as well as web loading speed.

There are many other image optimisation techniques exist in the industry such as lazy loading the image, using embedded SVG and etc to further allow you to put more images in your webpage without compromise the performance.

Here is the complete code for this tutorial if you interested.

Feel free to leave the comment below.

Create React App v5 is now comes with TailwindCSS v3

Great news people! Create React App v5 is just released with a bunch of upgrades such as support for Webpack 5, PostCSS 8, and one of my favourites is that it comes with TailwindCSS v3.

You are no longer in need of craco or react-app-rewired to get TailwindCSS work with CRA. All you need is just a few simple steps and let me show you.

  1. Create a new react project.
npx create-react-app my-app
  1. Initialize TailwindCSS.
npx tailwindcss init -p
  1. Update tailwindcss.config.js.
module.exports = {
  content: [
++  "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
  1. Import TailwindCSS’s directives in index.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
// Other styles

Voila! Now you can use TailwindCSS in your React app as you like. Feel free to check out the full changelog of Create React App v5. Also worth to mention TailwindCSS new JIT engine is another cool thing to try on too!

Translate Your React app with react-i18next

Localisation is very important for every web application to bring a better user experience to all the users from around the globe and not lose your potential customers.

Today, we will look into how to integrate localisation into your React web app including how to handle language changes, formatting, as well as how to load your localisation file from CDN and etc.

First, let’s start with a simple React app with language selections and a simple text display. You can clone or download the starter project to start with, or if you are impatient, you can get the completed code here.

Setting up react-i18next

Once you have the starter project, install the react-i18next dependency:

yarn add i18next react-i18next

Next, we need to setup react-i18next in our app:

1. Create an i18n.config.js inside the src folder. (Actually, you can name anything you want).

2. Add the following codes into the file.

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

import en from './resources/en.json';
import zh from './resources/zh.json';

i18n
  .use(initReactI18next) // passes i18n down to react-i18next
  .init({
    resources: {
      en: {
        translation: en, // Add translation for English
      },
      zh: {
        translation: zh, // Add translation for Chinese
      },
    },
    fallbackLng: "en",
    interpolation: {
      escapeValue: false // No need to escape for react
    }
  });

3. Import the config in App.js to initialize

// Other import
import i18next from 'i18next';
import './i18n.config';

// Rest of the code

4. Create 2 new files in src/resources/en.json and src/resources/zh.json and insert the content as below.

// src/resources/en.json
{
  "welcome": "Welcome to the world of wonder"
}

// src/resources/zh.json
{
  "welcome": "欢迎来到奇幻的世界"
}

Translate using useTranslation hook

To start translating, you can use useTranslation hook provided by react-i18next. Now let’s add our first translated text inside section tag in App.js:

const App = () => {
++const { t, i18n } = useTranslation();
  // Other code
  const handleLanguageSelect = (event) => {
    setSelectedLanguage(event.target.value);
++  i18n.changeLanguage(event.target.value);
  };
  // Other code

  return (
    <div className="h-screen flex justify-center items-center">
      <div className="mx-auto bg-white p-4 rounded space-y-2">
        // Other code
        <section>
++        <p>{t('welcome')}</p>
        </section>
      </div>
    </div>
  );
};

To change the language, you can always use the changeLanguage method from i18n .

Now, you should be able to see the welcome text is translated based on the language you have selected.

Interpolation

Interpolation is a very useful and common feature that we will be used in translation, it allows you to add dynamic values to your translated text.

Let’s say we want to display what language has been selected to the users.

const App = () => {
  // Other code

  return (
    <div className="h-screen flex justify-center items-center">
      <div className="mx-auto bg-white p-4 rounded space-y-2">
        // Other code
        <section>
          <p>{t('welcome')}</p>
++        <p>{t('selectedLanguage', { language: selectedLanguage })}</p>
        </section>
      </div>
    </div>
  );
};

Then add the new translation text in src/resources/en.json and src/resources/zh.json .

// src/resources/en.json
{
  "selectedLanguage": "Your selected language is: {{language}}"
}

// src/resources/zh.json
{
  "selectedLanguage": "您选择的语言是: {{language}}"
}

The differences between the welcome text and the selectedLanguage text is that we pass the language as the second parameter for t method to replace the placeholder in the translation text.

You should see something like this now.

For more details about interpolation, please check the official i18next interpolation documentation.

Formatting

Another powerful feature that i18n provides is you can format the interpolation value. You can either use built-in formatting functions based on Intl API or build your own format function.

To have a better understanding of formatting, we will build a simple uppercase format function. Now, add a simple format method in src/i18n.config.js :

// Other codes
i18n
  .use(initReactI18next)
  .init({
    // Other codes

    interpolation: {
      escapeValue: false,

++    format: (value, format, lng) => {
++      if (format === 'uppercase') {
++        return value.toUpperCase();
++      }
++
++      return value;
      }
    }
  });

The function is really self-explanatory: if the format is uppercase, it will convert the value to uppercase, otherwise, it just returns the value.

Next, we can apply the uppercase format in our selectedLanguage text, let’s update the text in src/resources/en.json and src/resources/zh.json :

// src/resources/en.json
{
--"selectedLanguage": "Your selected language is: {{language}}"
++"selectedLanguage": "Your selected language is: {{language, uppercase}}"
}

// src/resources/zh.json
{
--"selectedLanguage": "您选择的语言是: {{language, uppercase}}",
++"selectedLanguage": "Your selected language is: {{language, uppercase}}"
}

If you refresh the page now, you could see the language text has been converted to uppercase.

For more information about formatting, you can refer back to i18next formatting document.

Plural

Another common case that we definitely need when doing the translation is pluralisation, and of course, i18next got it for you too. We will simply count how many times have we changed the language to demonstrate the pluralisation in i18next.

First, let’s add the necessary text in src/resources/en.json and src/resources/zh.json.

// src/resources/en.json 
{
++"numOfTimesSwitchingLanguage": "Your have switch language for {{count}} time",
++"numOfTimesSwitchingLanguage_zero": "Your have switch language for {{count}} time",
++"numOfTimesSwitchingLanguage_other": "Your have switch language for {{count}} times"
}

// src/resources/zh.json 
{
++"numOfTimesSwitchingLanguage": "您已更换了语言{{count}}次",
++"numOfTimesSwitchingLanguage_zero": "您已更换了语言{{count}}次",
++"numOfTimesSwitchingLanguage_plural": "您已更换了语言{{count}}次"
}

Next, let’s add the logic for counting the number of times we change the language in App.js .

const App = () => {
  const { t, i18n } = useTranslation();
  const [selectedLanguage, setSelectedLanguage] = useState('en');
++const [count, setCount] = useState(0);

  const handleLanguageSelect = (event) => {
    setSelectedLanguage(event.target.value);
    i18n.changeLanguage(event.target.value);
++  setCount(count => count + 1);
  };

  return (
    <div className="h-screen flex justify-center items-center">
      <div className="mx-auto bg-white p-4 rounded space-y-2">
        // Other codes
        <section>
          <p>{t('welcome')}</p>
          <p>{t('selectedLanguage', { language: selectedLanguage })}</p>
++        <p>{t('numOfTimesSwitchingLanguage', { count })}</p>
        </section>
      </div>
    </div>
  );
};

Let’s refresh the page and see the outcome.

Great! we can see it changes to plurals once the count is more than 1. One important note here is that we must use count in order for pluralisation to work.

You can do more complex pluralisation with i18next. Please feel free to check out the i18next plural document if you like to.

Persisting and Detecting Language

We have explored a couple of functionality of i18next, but there is one imperfection of our language changes app because it does not persist in the language selection after you refresh the page. Luckily, there is a i18next-browser-languagedetector plugin that going to help us achieve this. This plugin also helps us to detect languages from cookies, URL, browser settings.

Now, let’s install the plugin.

yarn install i18next-browser-languagedetector

Then, add the plugin in src/i18n.config.js:

// Other import
++import LanguageDetector from 'i18next-browser-languagedetector';

i18n
  .use(initReactI18next)
++.use(LanguageDetector)
  .init({
    // Other configs
  });

Previously, we have set en as the default language, but now we want to use i18next.language as the default language. Thus, let’s make some changes in App.js.

const App = () => {
  const { t, i18n } = useTranslation();
--const [selectedLanguage, setSelectedLanguage] = useState('en');
++const [selectedLanguage, setSelectedLanguage] = useState(i18n.language);
  const [count, setCount] = useState(0);
  // Other codes
};

Alright, we can try to switch language to zh and refresh the page now. We should be able to see the default language stays at zh.

Loading Translation Texts From Server

If you’re working with translators or to get better performance with CDN, you may want to load the translation texts from CDN instead of embedding everything in your code.

To do this, i18next-http-backend plugin is here to help us load the translation texts from the server easily.

First of all, let’s install the plugin.

yarn install i18next-http-backend

Then, we need to change some of the configurations in i18n.config.js.

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
--import en from './resources/en.json';
--import zh from './resources/zh.json';
import LanguageDetector from 'i18next-browser-languagedetector';
++import HttpApi from 'i18next-http-backend';

i18n
  .use(initReactI18next)
  .use(LanguageDetector)
++.use(HttpApi)
  .init({
--  resources: {
--    en: {
--     translation: en,
--    },
--    zh: {
--     translation: zh
--    },
--  },
    fallbackLng: 'en',

    interpolation: {
      escapeValue: false,
      /**
       * Add interpolation format method to customize the formatting
       */
      format: (value, format, lng) => {
        if (format === 'uppercase') {
          return value.toUpperCase();
        }

        return value;
      },
    },

++  backend: {
++    loadPath: '/resources/{{lng}}.json',
++  },
  });

Basically, we have made 2 changes: first, we tell the i18n to use the plugin and set the loadPath to /resources/{{lng}}.json where lng will be replaced by the currently selected language code. Secondly, we also remove resources key because it is no longer being used as we are loading the translation texts remotely.

Next, we need to move the src/resources folder to public/resources folder so that the files can be loaded publicly.

Lastly, we need to wrap our application with Suspense in index.js.

import React, { Suspense } from 'react';
// Other codes

ReactDOM.render(
  <React.StrictMode>
++  <Suspense fallback="Loading...">
      <App />
++  </Suspense>
  </React.StrictMode>,
  document.getElementById('root')
);

In practice, we may want to add Suspense around a specific component instead of at the root level. However, for demo purposes, we can just wrap it in the root level.

If you refresh the app, it should work as normal.

Bonus – Namespaces

Speaking of performance, there is another thing that you can do to speed up the loading time of your long translation texts if your application grows large, and that is namespacing. Namespacing allows you to load the small chunk/group of translation texts at a time when you need without preloading everything upfront which is a waste as well as slow down the first load time.

You just need to change your i18next-http-backend loaded path to use the namespaces and add the namespace as the argument to useTranslation .

// i18n.config.js
{
  backend: {
--  loadPath: '/resources/{{lng}}.json',
++  loadPath: '/resources/{{lng}}/{{ns}}.json',
  }
}

// App.js
-- const { t, i18n } = useTranslation();
++ const { t, i18n } = useTranslation('YOUR_NAMESPACE');

To understand more about namespacing, please check out i18next namespaces documentation.

Conclusion

i18next is a robust and sustainable solution to localise your web application. It has many features and plugins to achieve what you need for localisation.

In this post, we are just exploring the tip of the iceberg, there is still much more to learn. I will suggest having a read on react-i18next and i18next documentation to learn how to use them in depth.

I hope you learned something today and please feel free to give any feedback/comment below!

Testing with React-Redux Hooks

With the rise of React Hooks, many developers have added hooks into their open source libraries. If you are using react-redux libraries, they also provided hooks such as useSelector and useDispatch as well.

Although hooks allow us to use redux with ease in React, however, there is not much documentation about unit testing with hooks. Thus, I’m going to show you how to unit test your react-redux hooks.

In class component, we can create a mock of the redux actions and states and pass them into the component for unit testing. As a matter of fact, there is not much difference in unit testing functional components with hooks. Instead of passing the mock state into the components, we can mock the implementation of `useSelector` to return state.

Imagine we have a component that looks like this:

import React from 'react';
import { useSelector } from 'react-redux';

const HelloWorld = () => {
  const user = useSelector(state => state.user);
  const skills = useSelector(state => state.skills);

  const { firstName } = user;

  return (
    <div>
      <p>First Name: {firstName}</p>
      <p>My Skills:</p>
      <ul>
        {skills.map(skill => (
          <p key={skill.id}>{skill.name}</p>
        ))}
      </ul>
    </div>
  );
}

export default HelloWorld;

We know useSelector is a function that takes a callback as the parameter. All we need to do is to mock the state and pass to the callback. The unit test would look like this:

import React from 'react';
import { render } from '@testing-library/react';
import { useSelector } from 'react-redux';
import HelloWorld from './HelloWorld';

jest.mock('react-redux', () => ({
  useDispatch: jest.fn(),
  useSelector: jest.fn(),
}));

describe('HelloWorld', () => {
  test('renders user with skills', () => {
    useSelector.mockImplementation((selector) => selector({
      user: {
        firstName: 'Tek Min',
      },
      skills: [
        {
          id: '1',
          name: 'Javascript',
        },
        {
          id: '2',
          name: 'React',
        },
      ],
    }));
    const { container } = render(<HelloWorld />);
    expect(container).toMatchSnapshot();
  });

  test('renders user without skills', () => {
    useSelector.mockImplementation((selector) => selector({
      user: {
        firstName: 'Tek Min',
      },
      skills: [],
    }));

    const { container } = render(<HelloWorld />);
    expect(container).toMatchSnapshot();
  });
});

If you check the snapshot, you will probably see something like this:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`HelloWorld renders user with skills 1`] = `
<div>
  <div>
    <p>
      First Name: 
      Tek Min
    </p>
    <p>
      My Skills:
    </p>
    <ul>
      <p>
        Javascript
      </p>
      <p>
        React
      </p>
    </ul>
  </div>
</div>
`;

exports[`HelloWorld renders user without skills 1`] = `
<div>
  <div>
    <p>
      First Name: 
      Tek Min
    </p>
    <p>
      My Skills:
    </p>
    <ul />
  </div>
</div>
`;

Great! we get the snapshot as we wanted, the components render correctly with the different redux states.

You may find another way of testing with redux hooks in Google, but I find that this is one of the simplest versions of doing it without additional libraries. Hope you find it helpful and have a nice day!

%%footer%%