Monthly Archives: March 2024

Zustand state management

I’m used to using Redux and more recently Redux Toolkit for global state management in React, however along with state management libraries such as Mobx there’s another library of interest to me, named Zustand. Let’s see how to set up and project and use Zustand and take a very high level look at how to set-up a project with Zustand…

Create yourself a React application, as usual I’m using TypeScript.

  • Add Zustand using yarn add zustand
  • Our store is a hook, and to create the store we use the create method, for example
    import { create } from "zustand";
    
    interface CounterState {
        counter: number;
        increment: () => void;
        decrement: () => void;
    }
    
    export const useCounterStore = create<CounterState>(set => ({
        counter: 0,
        increment: () => set(state => ({ counter: state.counter + 1 })),
        decrement: () => set(state => ({ counter: state.counter - 1 })),
    }));
    

The above creates a simple store, with state and methods to interact with the state. As we’re using TypeScript, we’ve declared the interface matching our state.

To use this state we simply use the hook like this (change App.tsx to look like the following)

import './App.css';
import { useCounterStore } from "./store";

function App() {
  const { counter, increment, decrement } = useCounterStore();

  return (
    <div className="App">
      <button onClick={increment}>+</button>
      <div>{counter}</div>
      <button onClick={decrement}>-</button>
    </div>
  );
}

export default App;

We can also get slices of our state using the hook like this

const counter = useCounterStore(state => state.counter);

Before we move on, unlike RTK we need to enable redux devtools if we want to view the state in the Redux DevTools in our Browser, so to add the dev tool extensions do the following

  • yarn add @redux-devtools/extension
  • We need to import the devtools and change our store a little, so here’s the store with all the additions
    import { create } from "zustand";
    import { devtools } from "zustand/middleware"
    import type {} from "@redux-devtools/extension";
    
    interface CounterState {
      counter: number;
      increment: () => void;
      decrement: () => void;
    }
    
    export const useCounterStore = create<CounterState>()(
      devtools (
        set => ({
          counter: 0,
          increment: () => set(state => ({ counter: state.counter + 1 })),
          decrement: () => set(state => ({ counter: state.counter - 1 })),
        }),
        {
          name: "counter-store",
        }
      )
    );
    

Zustand also has the ability to wrap our global state within a persistence middleware. This allows us to save to various types of storage. We simply wrap our state in persist like this

import { devtools, persist } from "zustand/middleware"

export const useCounterStore = create<CounterState>()(
  devtools (
    persist (
      set => ({
        counter: 0,
        increment: () => set(state => ({ counter: state.counter + 1 })),
        decrement: () => set(state => ({ counter: state.counter - 1 })),
      }),
      {
        name: "counter-store",
      }
    )
  )
);

By default (as in the above code) this state will be persisted to localStorage.

Go check your Application | Local Storage in Edge or Chrome developer tools, for example for Local Storage I have a key counter-store with the value {“state”:{“counter”:4},”version”:0}.

Code

Code from this post is available on github.

React with Signals

Signals are another way of managing application state.

You might ask, “well great but we already have hooks like useState so what’s the point?”

In answer to the above perfectly valid question, Signals work on a in a more granular way – let’s compare to useState. If we create a simple little (and pretty standard) counter component we can immediately see the differences.

Create yourself a React application, I’m using yarn but use your preferred package manager.

yarn create react-app react-with-signals --template typescript

Add the signal package using

yarn add @preact/signals-react

Now we’ll create a folder named components and add a file named CounterState.tsx which looks like this

import { useState } from "react";

export const CounterState = () => {
  const [count, setCount] = useState(0);

  console.log("Render CounterState");

  return (
    <div>
      <div>Current Value {count}</div>
      <div>
        <button onClick={() => setCount(count - 1)}>Decrement</button>
        <button onClick={() => setCount(count + 1)}>Increment</button>
      </div>
    </div>
  );
}

It’s not very pretty but it’s good enough for this demonstration.

Finally create a new file in the components folder named CounterSignals.tsx which should look like this

import React from 'react';
import { useSignal } from "@preact/signals-react";

export const CounterSignals = () => {
  const count = useSignal(0);

  console.log("Render CounterSignals");

  return (
    <div>
      <div>Current Value {count}</div>
      <div>
        <button onClick={() => count.value--}>Decrement</button>
        <button onClick={() => count.value++}>Increment</button>
      </div>
    </div>
  );
}

As you can see, the Signals code creates a Signals object instead of the destructuring way used with useState and the Signals object will not change, but when we change the value, the value within the object changes but does not re-rendering the entire component each time.

Let’s see this by watching the console output in our preferred browser, so just change App.tsx to look like this

import React from ‘react’;
import { CounterState } from ‘./components/CounterState’;
import { CounterSignals } from ‘./components/CounterSignals’;

function App() {
return (


);
}

export default App;
[/em]

Start the application. You might wish to disable React.StrictMode in the index.tsx as this will double up the console output whilst in DEV mode.

Click the Increment and Decrement buttons and you’ll see our useState implementation renders on each click whereas the counter changes for the Signals version but no re-rendering happens.

Code

Code for this example using Signals can be found on github.

i18n in React

i18n (internationalization) is the process of making an application work in different languages and cultures. This includes things like, translations of string resources through to to handling date formats as per the user’s language/culture settings, along with things like decimal separators and more.

There are several libraries available for helping with i18n coding but we’re going to focus on the react-i18next in this post, for no other reason that it’s by other teams in the company I’m working for at the moment.

Getting Started

The react-i18next website is a really good place to get started and frankly I’m very likely to cover much the same code here, so their website should be your first port of call.

If you’re want to instead follow thought the process here then, let’s create a simple React app with TypeScript (as is my way) using

npx create-react-app i18n-app --template typescript

Now add the packages react-i18next and i18next i.e.

npm install react-i18next i18next --save

In this Getting Started I’m going to also include the following libraries which will handle loading the translation files and more

npm install i18next-http-backend i18next-browser-languagedetector --save

Adding localized strings

We’re going to first show an example of embeding the string into a .ts file, however it’s much more likely we’ll want them in a separate file (or multiple files). Let’s get this started by creating a file named i18n1.ts.

In the file we’ll just write one string and here it is

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources: {
      en: {
        translation: {
          welcome: "code: Hello string en World"
        },
      },
      fr: {
        translation: {
          welcome: "code: Bonjour string fr World"
        }
      }
    },
    debug: true,
    interpolation: {
      escapeValue: false, 
    },
  });

export default i18n;

As you can see we’re using the LanguageDetector to automatically set our resources. On my browser the language is set to en-GB so my expectation is to see the en strings as I’ve not listed en-GB specific strings.

IMPORTANT: Before we can use this, go to index.tsx and import this file import “./i18n1”; so the bundler includes it.

Display/using our localized strings

Now we’ll need to actually use our translation strings. react-i18next comes with hooks, HOC’s and standard JS type functions to interact with our translated strings. Let’s clear out most of the App.tsx and make it look like this

import React from "react";
import "./App.css";
import { useTranslation } from "react-i18next";

const lngs: any = {
  en: { nativeName: "English"},
  fr: { nativeName: "French"},
}

function App() {
  const { t, i18n } = useTranslation();

  return (
    <div className="App">
      <div className="App-header">
        {t("welcome")}
        <div>
          {Object.keys(lngs).map(lng => {
            return <button key={lng} style={{margin: "3px"}}
              onClick={() => i18n.changeLanguage(lng)} disabled={i18n.resolvedLanguage === lng}>{lngs[lng].nativeName}</button>
          })}
        </div>
      </div>
    </div>
  );
}

export default App;

This code will simply display the English/French buttons to allow us to change the language as we wish. Notice, however, that if we add a new language, such as de: { nativeName: “German”} and no strings exist for that language, you’ll end up seeing the “key” for the string. We can solve this later.

At this point if all is working, you can start the application up and switch between the languages. You’ll see that the strings will be prefixed with code: just to make it clear where the strings are coming from, i.e. our code file.

Moving to resource type files (part 1)

As I mentioned, we probably don’t want to embed our string in code. It’s preferable to move them into their own .JSON files.

Create a folder within the src folder named locales (the names of the folders and files doesn’t really matter but it’s good to be consistent) and within that we’ll have one folder name en-GB and another named fr. So the English strings are specific to GB but the French covers all French languages locales.

Now in en-GB create the file translations.json which will look like this

{
  "welcome": "src: Hello en-GB World"
}

For the French translations, add translations.json to the fr folder and it should look like this

{
 "welcome": "src: Bonjour fr World"
}

Note the src: prefix is again, just there to allow us to see where our resources are coming from in this demo.

We now need to change our i18n.ts file to look like this

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";

import enGB from "../src/locales/en-GB/translation.json";
import fr from "../src/locales/fr/translation.json";

const resources = {
  en: {
    translation: enGB
  },
  fr: {
    translation: fr
  }
};
i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    debug: true,
    interpolation: {
      escapeValue: false, 
    },
  });

export default i18n;

So in this code we’reimporting the JSON and then assigning to the resources const. This is very similar to the other way we bought the resources into the i18n.ts file, just we’re importing via JSON files.

Moving to resource type files (part 2)

There’s another way to import the translated strings and that is to simply include the files in the public folder of our React application. So in the public folder add the same folders and files, i.e. locales folder with en-GB and fr folders with the same translation.json files as the last example. I’ve changed the src: prefix on the strings to public: again just so I can prove, for this demo, where the strings originate from.

In other words, here’s the en-GB translations.json file

{
  "welcome": "public: Hello en-GB World"
}

and the fr translations.json file

{
  "welcome": "public: Bonjour fr World"
}

Now back in our i18n.ts file change it to look like this

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";

i18n
  .use(Backend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: {
      "en" : ["en-GB"]
    },
    debug: true,
    interpolation: {
      escapeValue: false, 
    },
  });

export default i18n;

Fallback

My browser is setup as English (United Kingdom) which is en-GB and all works well. But what happens if we add a detect a language where we have no translation strings for? Well let’s try it by adding a German option to the lngs const in our App.tsx, so it looks like this

const lngs: any = {
  en: { nativeName: "English"},
  fr: { nativeName: "French"},
  de: { nativeName: "German"},
}

Now, what happens ? Well we’ve see the key for the string, which is probably not what we want in production, better to fall back to a known language. So we need to change the i18n.ts file and add a fallbackLng, I’ll include the whole i18n object so it’s obvious

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    lng: "en-GB",
    fallbackLng: "en",
    debug: true,
    interpolation: {
      escapeValue: false, 
    },
  });

Now if our application encounters a locale it’s not setup for, it’ll default to the fallback language (in this case) English. We can also achieve this using

fallbackLng: {
  "default": ["en"]
},

Using this syntax we can also map different languages to specific fallback languages, so for example we might map Swiss locale to map to use French or Italian. This is achieved by passing an array of fallback languages (as taken from the <a href="https://www.i18next.com/principles/fallback" rel="noopener" target="_blank">Fallback documentation</a>

[code]
fallbackLng: { 
  "de-CH": ["fr", "it"], // French and Italian are also spoken in Switzerland
},

Finally with regards fallback languages, we can write code to determine the translation to use, again the Fallback documentation has a good example of this, so I’d suggest checking that link out.

Code

Code for this post is available on github which includes i18n files 1-3 for each of these options for creating translations. just change the index.tsx to import the one you wish to use.