Category Archives: React

Dockerize your React application

You’ve created you React application and are now looking to create a docker image for it.

Before we look at the Dockerfile, create yourself a .dockerignore file that looks like this

.git
node_modules

We do not require the .git folder in our image and we’re going to install our node modules via npm install as you’ll find copying the node_modules folder(s) is slow.

Let’s jump straight in and look at a Dockerfile that will take our React code containerize it and set the container up to run nginx to host it.

FROM node:21-alpine3.18 as build

WORKDIR /usr/app
COPY . .
RUN npm install
RUN npm run build

FROM nginx:alpine
COPY --from=build /usr/app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

We’re using a node alpine image as our base. This is a lightweight image as we won’t our image to be lean. Next we create our WORKDIR, this can be set to your preferred location but just remember to reuse that location in the COPY command. We’re using .dockerignore to ignore .git and node_modules which allows us to then just copy everything to the image, where we then install then build our React application.

Finally we use nginx to serve our application, first copying the build folder to nginx and finally we set up the CMD to run nginx once the container is started, thus hosting our React application.

To build our image we can just use the following (change the tag name and version to suit your application)

docker build -t my-app:0.1.0 .

and to run, again set your application name any port redirects and use you tagged and version

docker run --rm --name my-app -p 8080:80 -d my-app:0.1.0

We could also extend this sample to include copying of nginx.conf to the image, for example if you want the supply a config like this

worker_processes 4;

events { worker_connections 1024; }

http {
  server {
    listen 4200;
    root  /usr/share/nginx/html;
    include /etc/nginx/mime.types;

  location / {
    root   /usr/share/nginx/html;
    index  index.html;
    try_files $uri $uri/ /index.html;
  }
}
}

If we assume we’re storing out nginx.conf file in a folder named .ngnix (this is not required it’s just for this example) then we could add the following to the Dockerfile, after the line FROM nginx:alpine add the following and whilst we’re at it let’s get rid of the default files that might be located in the images nginx/html folder

COPY ./.nginx/nginx.conf /etc/nginx/nginx.conf
RUN rm -rf /usr/share/nginx/html/*

Docker Compose

Whilst we’re here, let’s create a simple docker-compose.yaml file for our new image.

version: '3.8'
services:
  front-end:
    build:
      context: ./ui
      dockerfile: ./ui/Dockerfile
    ports:
      - 4200:4200
    image: putridparrot/my-app:0.1.0
    container_name: my-app

We might like to store configuration for this image on the hosting server, i.e. via a volume in which case we’d simply add to the bottom of this file the following

    volumes:
      - ./ui/public/appsettings.json:/usr/share/nginx/html/appsettings.json

In this example we’re using an appsettings.json file to configure the environment, or it might include feature flag settings or whatever and assuming it’s stored in the public folder of you React application.

Now we just docker-compose up.

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.

Signal R and React

I haven’t touched Signal R in a while. I wanted to see how to work with Signal R in a React app.

Let’s start by creating a simple ASP.NET Core Web API server

  • Create an ASP.NET Core Web API application, I’m going to use minimal API
  • Add the NuGet package Microsoft.AspNetCore.SignalR.Client
  • Add the following to the Program.cs
    builder.Services.AddSignalR();
    builder.Services.AddCors();
    
  • We’ve added CORS support as we’re going to need tthis for testing locally, we’ll also need the following code
    app.UseCors(options =>
    {
      options.AllowAnyHeader()
        .AllowAnyMethod()
        .AllowCredentials()
        .SetIsOriginAllowed(origin => true);
    });
    

Before we can use Signal R we’ll need to add a hub, I’m going to add a file NotificationHub.cs with the following code

public class NotificationHub : Hub;

Now return to Program.cs and add the following

  • We need to map our hub into the application using
    app.MapHub<NotificationHub>("/notifications");
    
  • Finally let’s map an endpoint to allow us to send messages via Swagger to our clients. After the line above, add the following
    app.MapGet("/test", async (IHubContext<NotificationHub> hub, string message) =>
      await hub.Clients.All.SendAsync("NotifyMe",$"Message: {message}"));
    

Now we need a client, so create yourself a React application (I’m using TypeScript as usual with mine).

  • Add the package @microsoft/signalr, i.e. from yarn yarn add @microsoft/signalr
  • In the App.tsx we’re going to create the HubConnectionBuilder against out ASP.NET Core API server. We’ll then start the connection and finally watch for messages on the “NotifyMe” name as previously set up in the ASP.NET app, the code looks like this
    import { HubConnectionBuilder } from '@microsoft/signalr';
    
    function App() {
      const [message, setMessage] = useState("");
    
      useEffect(() => {
        const connection = new HubConnectionBuilder()
          .withUrl("http://localhost:5021/notifications")
          .build();
      
        connection.start();  
        connection.on("NotifyMe", data => {
          setMessage(data);
        });
      }, [])  
    
      return (
        <div className="App">
          {message}
        </div>
      );
    }
    

Make sure you start the ASP.NET server first, then start your React application. Now from the Swagger page we can send messages into the server and out to the React client’s connected to SignalR.

Using the Redux Toolkit

I’ve covered Redux on this blog in the past, but the “modern” approach is to use the Redux Toolkit which cleans up and simplifies some of the code. In some cases I found it more complicated, because it obviously does a lot and abstracts a lot.

Going over the basics

Let’s go over some of the basics of Redux first…

Redux is a global state management system. It’s made up of three key parts, the store which as the name suggests, is where your state is stored. The shape of the data within the store is defined by your needs (i.e. no prerequisites). The store is used for global state, so to partition the store for ease of working with your data, we create slices. We’ll generally have slices based upon the domain, so maybe a slice for feature flags, another for component state, another for an API and so on.

Next we have actions, these are (as the name suggests) used to perform actions against our data. In Redux we have an object with a type and some (optional) payload. The type is the name of the action,

{type: "INCREMENT", payload: 3 }

Finally we have reducers which we use, based upon the type of actions, to carry out updates of state. It’s important to remember that Redux data should be immutable, hence the reducer doesn’t change the state itself, but instead copies the state updating the new version of the state before passing this new copy back

RTK

Let’s now create a bare minimum Redux toolkit store

Note: I’m using TypeScript in my examples

import { configureStore } from "@reduxjs/toolkit"

export const store = configureStore({
  reducer: {}.
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

The code above just creates a Redux store, there’s no reducers. This store is not too useful at this point, but bare with me, let’s now quickly pivot to look at how we might use our store in a React app

import { Provider } from "react-redux"
import { store } from "./state/store.ts"

<Provider store={store}>
   <App />
</Provider>

By using the Provider at the App level our store is now available to our entire app.

As mentioned, the store above is pretty useless at the moment, let’s use the example from Redux/RTK to add a simple counter to the store. This represents one slice of the store hence, we’ll name the file counter.slice.ts (although RTK tends to go with counterSlice.ts style names).

First create a folder for your slices within the store folder, i.e. store/slices. Ofcourse you can place these where best suit, but this is the folder structure I’m using here.

Create the file counter.slice.ts inside store/slices. This, as you probably realised, will contain the counter slice within the state manager, i.e. the data specific to the counter

import { createSlice } from "@reduxjs/toolkit";

interface CounterState {
  value: number;
}

const intialState: CounterState = {
  value: 0;
};

const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    }
  },
});

export const { increment, decrement } = counterSlice.actions;

export default counterSlice.reducer;

In this example we appear to be mutating state, but behind the scenes createSlice handles copying our state (RTK uses Immer to handle such immutability), if you prefer you can use the more obvious state.value + 1.

This file includes the CounterState interface (obviously not required for pure JavaScript) which is a representation of what our counter state will look like, in this case we just store a number, the current counter value. We also have these actions within the reducer, increment and decrement.

We create an initialState so our store has any defaults or starting points, for example in this case we set count initially to zero.

Next, we need to export our actions using export const { increment, decrement } = counterSlice.actions and finally for this section, we now need to register the slice with the store, this is accomplished by adding it to the store reducer, like this

import counterReducer from "./counter/counterSlice";

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  }
});

At this point, everything is hooked up in the store, so let’s use our increment/decrement actions. We use the useSelector to get our current state and useDispatch to dispatch the actions to the the store

import { useSelector } from "react-redux";
import { RootState } from "../state/store";
import { decrement, increment } from "../state/counter/counterSlice";

export const Counter = () => {
  const count = useSelector((states: RootState) => state.counter.value);
  const dispatch = useDispatch(); // dispatch actions

  return (
    <div>
      <div>{count}</div>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
    </div>

}

Passing parameters

These examples are using simple actions. Let’s extend then by adding a incrementBy

Add a new action to counterSlice like this

incrementBy: (states, action: PayloadAction<number>) => {
  state.value += action.payload;
}

<em>Note: Whilst the += makes it look like we're mutating state, this is just a reminder, in this instance code within RTK is handling this as an immutable update.</em>

The PayloadAction only handles a single parameter, hence if we want to handle multiple parameters, we need to create an interface (in TypeScript) to define them. Let's move from the counter example and assume our Router is accepting multiple query parameters which we want to store in our store. We'll use an example of the github URL where by you can access my blog post code via <a href="https://github.com/putridparrot/blog-projects" rel="noopener" target="_blank">https://github.com/putridparrot/blog-projects</a>, so the first parameter might be gitUser, in this case <em>putridparrot</em>, followed by the repo name - hence we'd create our queryParams.slice.ts as follows

[code]
import { PayloadAction, createSlice } from "@reduxjs/toolkit";

export interface IQueryParams {
  gitUser?: string;
  repoName?: string;
}

const initialState: IQueryParams = {
  gitUser: "",
  repoName: "",
}

const queryParamsSlice = createSlice({
  name: "queryParams",
  initialState: initialState,
  reducers: {
    setQueryParams: (_, action: PayloadAction<IQueryParams>) => 
      action.payload,
  },
});

export const { setQueryParams } = queryParamsSlice.actions;

export default queryParamsSlice.reducer;

Everything’s pretty much as our counter example apart from the PayloadAction<IQueryParams>. In this example we’re actually just passing everything into the store so just returning the payload via setQueryParams.

Async actions

RTK supplies a way to incorporate async actions into the store. If we have something like a network call or another process we know may take some time, we’ll want to make it async. Just for a simple example, we’ll assume increment is now handled via either a network call or some longer running piece of code. We create an async thunk like this

export const incrementAsync = createAsyncThunk(
  "counter/incrementAsync",
  async (amount: number) => {
    // simulate a network call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    return amount;
  }
);

We’re supplying the name of our function counter/incrementAsync here. We now need to add this function to our slice, so after the reducers we add the following

extraReducers: (builder)  => {
  // we can accept state but for this example, we've no need
  builder.addCase(incrementAsync.pending, () => { 
    console.log("Pending");
  }).addCase(incrementAsync.fulfilled, (state, action: PayloadAction<number>) => {
    state.value += action.payload;
  });
}

Notice that each “case” is based upon some state which comes from createAsyncThunk. This is very cool as we now have the ability to update things based upon the current state of the async call, i.e. maybe we display a spinner for pending and remove when fulfilled or rejected occurs and in the case of rejected we now display an error toast message or the

Dispatch

To actually call into the store, i.e. call and action, we need to use code like the following

const dispatch = useDispatch<AppDispatch>();

Usually we’ll just wrap both the dispatch and selector into the following to make working with TypeScript simpler (just place this is some utility file such as hooks.ts or the classic helper.ts named file)

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

So now we can get the current count and update like this

import { decrement, increment } from '../store/slices/counter.slice'
import { useAppDispatch, useAppSelector } from '../store/hook'

export function Counter() {
  const count = useAppSelector((state) => state.counter.value)
  const dispatch = useAppDispatch()

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

Calling services or API’s

The example within the RTK documentation is a call to the pokeapi, so let’s start with that. RTK documentation seems to suggest such API’s go into a folder named services, so we’ll stick with that although they are essentially just other slices of global state (at least in my view).

In our services folder add pokemin.ts and here’s the contents of the file

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const pokemonApi = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
  tagTypes: [],
  endpoints: (builder) => ({
    getPokemonByName: builder.query({
      query: (name: string) => `pokemon/${name}`,
    }),
  }),
})

// Export hooks for usage in functional components
export const { useGetPokemonByNameQuery } = pokemonApi

We used createAsyncThunk earlier and ofcourse we can easily implement services/network/API calls via the same mechanism, but the createApi method, which extends createAsyncThunk, generates React hooks for us. We also have options to handle caching and more from here.

In this example we use the fetchBaseQuery defining the baseUrl. This is a lightweight method wrapping the fetch method.

We define our endpoints, i.e. the API to our service and then at the end of the code we export the code generation hook, useGetPokemonByNameQuery.

To use this code within our store we need to go back to store.ts and add the following to the rootReducer

api: pokemonApi.reducer,

and to the middleware we need to concat the API like this

getDefaultMiddleware()
  .concat(pokemonApi.middleware),

Finally we’ll want to use this API, and we simply use the hook like this

export const Pokemon = ({ name, pollingInterval }: { name: string, pollingInterval: number }) => {
  const { data, error, isLoading, isFetching } = useGetPokemonByNameQuery(
    name,
    {
      pollingInterval,
    },
  );

Notice that we now get those async thunk states, but named differently. So we have the data returned, a potential error. We also have isLoading and isFetching for handling progress info.

Multiple parameters and multiple API’s

As mentioned previously RTK accepts a single object as input and this is the same for createApi actions, so if you have a situation where you need to pass multiple parameters to your API, then you handle in the same way (although in this example I’m creating via an anonymous type), i.e.

getContract: builder.query<any, {contractId?: string, version?: string}>({
  query: (arg) => {
    const { contractId, version } = arg;        
    return {
      url: `someurl?contractId=${contractId}&version=${version}`,
   }
  },

With regards multiple API’s, let’s say we have a pokemon and contracts api (I admit an odd combination). Then to register them with the store, as seen with a single API we concat them with the middleware, but we don’t appear to concat each, instead we concat an array like this

getDefaultMiddleware()
  .concat([contractsApi.middleware,  pokemonApi.middleware]),

Dynamic baseUrl and/or setting headers etc.

In the pokemon API example we had a fixed URL for the service, but when developing the front and back end we’ll often have dev and/or test and then production environments. In this case we’ll want to change the baseUrl based upon the environment. In such cases and also when we want to apply our own headers, for example let’s assume our API requires an SSO token, then we’ll want to include that in the call in some way or other.

Here’s our dynamicBaseQuery which we’re actually using the store to store something called enviroment which we’re assume contained data such as the the current environment URL along with the current SSO token for the user for the environment. In the second line we set the baseUrl and then it’s used in the fetchBaseQuery. We also use this to set up the headers.

const dynamicBaseQuery: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> = async (args, WebApi, extraOptions) => {
  const baseUrl = (WebApi.getState() as any).environment.appSettings.contractsApiUrl;
  const rawBaseQuery = fetchBaseQuery({
    baseUrl,
    prepareHeaders: async (headers, { getState }) => {
      const token = (getState() as any).environment.token;
      if (headers) {
        headers.set("Accept", "application/json");
        if(token) {
          headers.set("authorization", `Bearer ${token}`);
        }
      }

      return headers;
    },
  });
  return rawBaseQuery(args, WebApi, extraOptions);
};

To use this we simple replace the baseQuery value with dynamicBaseQuery, like this snippet

export const contractsApi = createApi({
  reducerPath: "contractsApi",
  baseQuery: dynamicBaseQuery,
  // rest of the code ommitted

Tooling

I highly recommend installing the Redux Dev Tools for your browser. These tools allow you to easily see the state of your Redux store (and as RTK is built on Redux we can see our RTK defined store) and best of all, RTK is automatically configured to work with the Redux Dev Tools, so now additional code is required.

Code

Code for this post is available in my blog-projects.

Note: Not all code is hooked up and being used, but hopefully there’s enough to show how things might look.

React Context API

The React Context API allows us to share data between components without the need for passing props to each child component. One of the biggest data problems with have in React is how to pass data down an element tree from parent to a child element. There’s several ways to achieve this including Redux and Mobx as well as the slightly more dreaded “prop drilling”.

Using the Context API we can essentially declare components which accept a specific context and in the parent we “provide” that context. This means child components will get context directly.

Let’s see how we implement a context…

We need a context (the data), a context provider and a context consumer.

  • Create a folder names contexts
  • We’ll create a simple string array for our data, so we create our context like this (mine’s in a file named NameContext.tsx
    import { createContext } from "react";
    
    export const NameContext = createContext<string[]>([]);
    

    In a real world application the NameContext file would have the types/interfaces for our application data, but we’re just making a simple example here.

  • Now we can wrap any part of our application in our NameContext, so in App.tsx I’ll have the following
    import { useState } from 'react';
    import './App.css';
    import { NameContextList } from './components/List';
    import { NameContext } from './contexts/NameContext';
    
    function App() {
      const [issues, _] = useState(["One", "Two", "Three"]);
    
      return (
        <NameContext.Provider value={issues}>
          <NameContextList />
        </NameContext.Provider>
      );
    }
    
    export default App;
    
  • How you handle this part is upto you, but I’m going to first create a List component which we can pass props to, then a context consumer – this will allow me to test the actual component without having to create a context provider, so my List.tsx looks like this
    import { NameContext } from "../contexts/NameContext";
    
    
    export const List = ({items}: {items : string[]}) => (
        <ul>
            {items.map(item => {
                return <li>{item}</li>    
            })}
        </ul>
    )
    
    
    export const NameContextList = () => (
        <NameContext.Consumer>
        {(ctx) => (
            <List items={ctx} />
        )}
      </NameContext.Consumer>
    )
    

    We can also use the hook useContext like this

    export const NameContextList = () => {
        const names = useContext(NameContext)
        return <List items={names} />
    }
    

When we run this application, the App.tsx creates some data via useState and supplies this to the provider. Obviously if you were fetching data from a service or API, facade or whatever, then you’ve ofcourse change this slightly, but the concept’s much the same. The context consumer will then receive the data and it’s not been passed down via the props.

Another addition to this might be to wrap the line

const names = useContext(NameContext);

in a method such as

export const useNameContext = () => {
  const context = useContext(NameContext);

  if(!context) {
     throw new Error("NameContext is in an invald state");
  }

  return context;
}

This then just allows us to write code (especially in Typescript) safe in the knowledge the context was created.

Suspense in React 18

Suspense is a JSX Element/Component which we use to wrap around child components such that whilst they are loading, we can display something else, like a progress page or other fallback page.

This has obvious uses when your React application starts up and potentially loads data from a remote source etc.

Suspense understands, what the React documentation calls “Suspense-enabled data sources”. These include data fetching frameworks such as Relay and Next.js. Also the lazy import keyword lazy and also can understand promises with the (currently experimental) use hook.

Before we use components within the Suspense boundary, they need to be lazy imported.

A simple example of usage would be

<Suspsense fallback={<Loading />}>
   <Header />
   <NavBar />
   <Content />
   <StatusBar />
</Suspsense>

In this example, the Suspense boundary will load the child components, Header, NavBar etc. whilst loading we’ll see the fallback component. Upon completion that will be removed and we’ll see our full application.

But as mentioned, we need to lazy import these components so we’re have imports like this

const Header = lazy(()=> import("./components/Header"));
const NavBar = lazy(()=> import("./components/NavBar"));
const Content = lazy(()=> import("./components/Content"));
const StatusBar = lazy(()=> import("./components/StatusBar"));

There’s a requirement for lazy imported code to exported as default. If you don’t want to or cannot change the export to default (for example if using micro frontends that you didn’t write) then we can get around this with slightly more verbose syntax.

const Header = React.lazy(() => import("header/Header")
   .then(module => ({ default: module.Header })));

lazy is returning a promise, we can use the continuation as shown above (the then clause), to assign the module’s export to the default: parameter.

Along with use of Suspense to load the children whilst display a fallback, lazy also allows components to be selectively loaded, i.e. not everything is loaded at once, which has obvious results in ensuring better performance when not all components need loading initially, so using Suspense we also now have a nice way to load parts of our application as and when required as distinct parts of an application, each with a fallback. With lazy we also have more options for code splitting to reduce bundle size etc.

Micro frontends using React and Module Federation

Module federation allows us to create micro frontends in a pretty simple way.

Start off by running

npx create-mf-app

You’ll be prompted for a few things (these may change in different versions of this app. so these are current settings)

  • Pick the name of your app. This is essentially the name of the shell/host or main application (however you prefer to think of it). So I’m used to Shell application in MAUI, Xamarin Forms, WPF etc. So mine’s going to be named shell.
  • Project Type. We’re going to choose Application (as this is our shell application)
  • Port number. The default is 8080, but I’m going to choose 3000 (as I already have servers running on 8080 for other things).
  • Framework. We’re going to be using react
  • Language. I’m a TypeScript person, so I’m choosing typescript
  • CSS Accept the default which is CSS

Now our shell will be created. So as the prompt will state

  • cd shell
  • npm install
  • npm start

If all worked you’ll be presented by some large text stating the name of the app, framework etc.

Let’s now create another app in exactly the same way, obviously give it a different name (mine’s about), this is one of your micro frontends, also give it a different port, i.e. 8081 if you stuck with the default 8080 for the shell or in my case 3001.

Now go to that application and run it up using the commands below (again)

  • cd about
  • npm install
  • npm start

If all went well we have an application (the shell) running and the about application running as well. So nothing too exciting to see here, but it just goes to show we’re developing “standard” React apps.

What we will now need to do, is create a component that we’re going to expose from this application. All I’m going to do is move the App.tsx code that shows the application name etc. into a component, so if you want to follow along, let’s add a components folder to the src folder and within that add a About.tsx file and moved the code from the App.tsx into it, so it looks like this

import React from "react";

export const About = () => 
(
    <div className="container">
    <div>Name: about</div>
    <div>Framework: react</div>
    <div>Language: TypeScript</div>
    <div>CSS: Empty CSS</div>
  </div>
);

Now the App.tsx looks like this

import { About } from "./components/About";

const App = () => (
  <About />
);

We need to make some changes to webpack.config.js, so locate the ModuleFederationPlugin section.

  • Locate the exposes section and change it to look like this
    exposes: {
      "./About": "./src/components/About"
    },
    

    We expose all the components we want via this section and the shell app can then access them.

  • In your shell/container application we also need to amend the webpack.config.js and locate the remotes section, as you’ve probably worked out, this is going to register the micro frontends to be used within the shell/container. So mine looks like this

    remotes: {
      about: "about@http://localhost:3001/remoteEntry.js"
    },
    

    Let’s see if this work, open the shell/container’s App.tsx and add/change it to look like the below

    import { About } from 'about/About';
    
    const App = () => (
      <div> 
      <div className="container">
        <div>Name: shell</div>
        <div>Framework: react</div>
        <div>Language: TypeScript</div>
        <div>CSS: Empty CSS</div>
      </div>
      <About />
      </div>
    );
    

    Run the about app and then the shell and if all went to plan you’ll see the “About” component in the shell.

    A little more in depth ModuleFederationPlugin

    This is really cool and we can see much of the work is done on the ModuleFederationPlugin, so let’s look a little more in depth into some of the key features

    • name is the name we’re giving to our application
    • library is used to determine how exposed code will be stored/retrieved
    • filename by default is remoteEntry.js, but we can name it whatever we want, for example remoteAbout.js for our about app
    • remotes, as we’ve seen these point to the “modules” we want to include and have the format
      "app-name": "name@remote-host/entryFilename.js"
      
    • exposes, as we’ve seen also the exposes is used to expose components etc. from our micro frontend, it has the format
      "name" : "location-within-app"
      
    • shared is used to share node libraries which the exposed module depends on.

    Code

    Checkout an example shell app with three Material UI/React micro frontend apps at microfrontend-react. Each app. uses Material UI and the shell app. brings them together to create the basics of an application with header, navigation bar and some content.

    Obviously there’s a lot more to do to have a truly interactive micro frontend based application, but it’s a starting point.

useState and useEffect in React

Since React’s move to using hooks instead of classes, two of the primary hooks we need to get used to using are useState and useEffect.

Note: I’m using Typescript for my React apps. so code listed will have types etc. but if you’re from a pure Javascript background it should still be pretty obvious what’s going on.

useState

So, in the time before hooks, we might use classes. We’d pass props into the constructor of the class and set state within the class. Hence, we might end up with something like this

interface IViewerProps {
   startAt: number;
}

interface IViewerState {
    counter: number;
}

export class Viewer extends React.Component<IViewerProps, IViewerState> {
   constructor(props: IViewerProps) {
      super(props);
         this.state = {
            counter:  props.startAt
         }

      this.onClick = this.onClick.bind(this);
   }

   onClick(): void {
      this.setState({
         counter: this.state.counter + 1
      });
   }

   render() {
      return (
         <div>
            <div>{this.state.counter}</div>
            <button onClick={this.onClick}>Click Me</button>
         </div>
      );
   };
}

In this instance we’d pass a startAt value via the props, assign to the internal state then update this internal state.

Functional Components

Now, the move to functional based components ofcourse would lose the ability to maintain state across function calls, unless we had some way to associate it with that function call. Whereas a class would handle this initialization within its constructor. In the case of a functional component, we need a way where subsequent calls to that function cannot reinitialize the state or – in the case of out button counter example, the state would simply be reset to the one supplied by the props each render.

Let’s look at recreating the class above but as a functional component.

export function Viewer(props: IViewerProps) {
   const [state, setState] = useState(props.startAt);

   function onClick(): void {
      setState(state + 1);
   }

   return (
      <div>
         <div>{state}</div>
            <button onClick={onClick}>Click Me</button>
         </div>
   );
}

Now in this case useState is initialized the first time it’s used with the props.startAt value. This initialization does not take place again, during the lifecycle of this function, so that when you click the button it updates the state and re-renders the component without reinitializing the state. We can see this by putting console.log(`Props: ${props.startAt} State: ${state}`); after the useState line. In this case you’ll see the props value remains constant but the state changes on each click of the button.

This is great. But, what happens if the parent control actually needs to change the props. So, for example maybe we click a reset button to reset the value to the default.

useEffect

Whilst useState allows us to store state between function calls on a React component, we need a way to handle side effects, or more specifically in this example, we need ways of changing the state when the props change.

Let’s assume our parent component can set and reset the initial state for our Viewer component via the props. In fact, here’s that App component to demonstrate this


function App() {
   const [state, setState] = useState(1);

   function onReset() {
      setState(state === 0 ? 1 : 0);
   }

   return (
      <div className="App">
         <header className="App-header">
            <Viewer startAt={state} onReset={onReset}/>
         </header>
      </div>
   );
}

Note: this is a silly contrived example as we need the props to actually change – but for real work usage, imagine at some point a change in your app. is stored to localStorage and maybe. onReset loads the latest from localStorage. If that props value has now changed it will not (at this time) be reflected in the Viewer render.

You can see we’re using useState to supply the state as props to our Viewer component from our App. If you load the app as it stands, you’ll see nothing changes on the page, from our original implementation. The Viewer will keep incrementing even when reset is clicked. This is because we have no way to reset the state (remember it’s created like it would be in a constructor, i.e. when the function was first called).

This is where we use useEffect. The useEffect hook allows us to respond to changes in the props (and/or other dependencies), by adding the following code below the useState line in the Viewer component

useEffect(() => {
   setState(props.startAt);
}, [props])

Now when the props change (the [props] code denotes useEffecthas a dependency on the props value) useEffect will call setState, updating it with the latest props. We could ofcourse make this more granular by just having a dependency on [props.startAt]. We can supply an array of dependencies, any of which changes will cause useEffect code to execute.

Note: Ofcourse with a React class-based component we will also have the issue of how to reinitialize state from the props, because the props are set via the constructor. Hence this is not an issue just for functional components but in these cases useEffect is an elegant solution.