Showing posts with label React. Show all posts
Showing posts with label React. Show all posts

Building a Tennis Scorekeeper App

Background

When watching my nephew’s junior tennis matches, I often forget what the score is. After looking at existing tennis scorekeeper apps and not seeing any with the features I wanted, I decided to build my own. In this post, I will talk about the app I built and the design decisions behind it.

Most junior tennis events I've watched play 1 set matches where the first player to win 4 games with tiebreak at 3-3 or 6 games with tiebreak at 5-5 is the winner. The players spin a racket at the start of the match to determine who serves first. At an event, a player usually plays 3 or 4 sets, each against a different opponent. This differs from pro tennis where matches are best of 3 sets (or 5 at grand slams) where each set is first to 6 games with tiebreak at 6-6.

Design

To support both junior and pro style scoring in my app, the user is given the option to play a tiebreak at the start of each game and can start a new set anytime. To support 1 set matches, the user is allowed to change the player serving first at the start of each set, which defaults to the receiver from the previous game. Since the user has more options, it is possible to make mistakes. For example, starting a new set at 4-0 when playing a 6 game set or giving a point to the wrong player. To handle these cases, I included an undo function. To persist the match state across page reloads and screen locks, all updates to the match state are saved to local storage.

Implementation

I chose to make the app a client side web app to leverage the browser’s built in IndexedDB storage for persisting match state. I used React to build the app.

Demo!

The app is available here.

Implementing OpenID Connect in React

Building authentication for a web app from scratch is not a trivial task. A good implementation may require support for complex authentication methods, account management functionality, and secure storage of user credentials. If the app calls a REST API for backend services, a secure mechanism for passing the identity of the user in requests to the API is also required. OpenID Connect Implicit Flow solves these challenges by delegating authentication to a third party and using tokens to encode and transport user identity. In this post, I will explain how it works and demonstrate how to implement it in a React app.

How it Works

In OpenID Connect Implicit Flow, 1) the app requests that the user authenticate by redirecting the user to a trusted third party to authenticate called the Auth Provider. 2) The user can authenticate by any method supported by the Auth Provider. 3) If successful, the Auth Provider redirects the user back to the app with a signed id_token encoding a set of claims about the user's identity such as the user's email. 4) The app includes this id_token in requests to the REST API for authentication. 5) When the REST API receives the request, it verifies the id_token's signature using the Auth Provider's public key and looks at the id_token's claims to determine the user associated with the request.

The location at the Auth Provider where the Webapp sends the auth request (step 1) is called the authorization_endpoint. Each auth request includes the client_id, which the Auth Provider uses to determine the source of the auth request. The location at the app where the Auth Provider sends the id_token (step 3) is called the redirect_uri. The authorization_endpoint can be found in the Auth Provider's Discovery Document. During registration with the Auth Provider, the app owner sets the redirect_uri and the app is assigned its client_id.

Implementation

In the React app, the oidc module contains functions for sending the auth request (sendAuthReq) and handling the id_token from the auth response (handleAuthResp). It uses the authorization_endpoint, client_id, redirect_uri defined in oidc config module. The TokenProvider component manages the token state and provides it to its child components, which are rendered by the RouterProvider according to the routes defined in its assigned router. The index component contains a login button which triggers authentication by calling sendAuthReq from its click handler. The callback component, which is mapped to the redirect_uri, calls handleAuthResp to get the id_token and setToken to update the token state. The Claims component gets the token and displays its claims about the identity of the authenticated user.

OIDC module


export const config = {
  authorization_endpoint: 'REPLACE_WITH_AUTHORIZATION_ENDPOINT',
  client_id: 'REPLACE_WITH_CLIENT_ID',
  redirect_uri: 'REPLACE_WITH_REDIRECT_URI',
};
src/auth/oidc.config.ts

import { decodeJwt } from "jose";
import { nanoid } from "nanoid";
import { config } from "./oidc.config";

export function sendAuthReq() {
  const {pathname} = new URL(window.location.href);
  localStorage.setItem('location', pathname);

  const nonce = nanoid();
  localStorage.setItem('nonce', nonce);

  const {authorization_endpoint, client_id, redirect_uri} = config;
  
  const params = new URLSearchParams({
    client_id, 
    response_type: 'token id_token',
    scope: 'openid profile email',
    redirect_uri, 
    nonce
  }).toString();
  const {href} = new URL(`${authorization_endpoint}?${params}`);
  
  window.location.href = href;
}

export function handleAuthResp() {
  const {hash} = new URL(window.location.href);
  if (!hash) {
    throw new Error('No fragment');
  }
  const fragment = new URLSearchParams(hash.substring(1));

  const id_token = fragment.get('id_token');
  if (!id_token) {
    throw new Error('No id_token');
  }
  
  const {nonce: received} = decodeJwt(id_token);
  const sent = localStorage.getItem('nonce');
  if (sent !== received) {
    throw new Error('Nonce mismatch');
  }

  return id_token;
}
src/auth/oidc.ts

Since sendAuthReq can be called from anywhere in the app, it saves the current location to localStorage for the Callback component to retrieve and navigate to after authentication. To protect against replay attacks, a nonce is included in the request, which the auth provider returns in the response as a claim in the id_token. The nonce sent is saved to localStorage for handleAuthResp to retrieve and verify that it matches the nonce received. Since the Auth Provider may support multiple authentication flows, the response_type is set to 'token id_token' to specify that this is a request for Implicit Flow. To request claims about the user's profile and email, the scope is set to 'openid profile email'. In handleAuthResp, the id_token is retrieved from the hash property of the redirect_uri.

TokenProvider


import { decodeJwt } from "jose";
import { createContext, useContext, useEffect, useState } from "react";
import { sendAuthReq } from "./oidc";

export const TokenContext = createContext<null|string>(null);
export const SetTokenContext = createContext((token: null|string) => {});

export default function TokenProvider({children}: any) {
  const [token, setToken] = useState<null|string>(null);

  // restore token from localStorage
  useEffect(() => {
    const stored = localStorage.getItem('token');
    if (stored) {
      const {exp} = decodeJwt(stored);
      if (exp && exp*1000 > Date.now()) {
        setToken(stored);
      }
    }
  }, []);

  // sync token with localStorage
  useEffect(() => {
    if (token) {
      localStorage.setItem('token', token);
    } else {
      localStorage.removeItem('token');
    }
  }, [token]);

  // schedule auth when token expires
  useEffect(() => {
    if (token) {
      const {exp} = decodeJwt(token);
      if (exp) {
        const expiresIn = exp*1000-Date.now();
        const id = setTimeout(sendAuthReq, expiresIn);
        return () => clearTimeout(id);
      }
    }
  }, [token]);

  return (
    <TokenContext.Provider value={token}>
      <SetTokenContext.Provider value={setToken}>
        {children}
      </SetTokenContext.Provider>
    </TokenContext.Provider>
  );
}

export function useToken() {
  return useContext(TokenContext);
}

export function useSetToken() {
  return useContext(SetTokenContext);
}
src/auth/TokenProvider.tsx

The token state and its setter are provided to TokenProvider's children using Context. The useToken and useSetToken custom Hooks are created to encapsulate the use of Context and simplify access to this state. To prevent the user from having to re-authenticate after reloading the app, the first two Effects sync the token state with localStorage. The third Effect schedules re-authentication when the token expires.


import React from 'react';
import ReactDOM from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import TokenProvider from './auth/TokenProvider';
import './index.css';
import { router } from './router';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <TokenProvider>
      <RouterProvider router={router} />
    </TokenProvider>
  </React.StrictMode>
);
src/index.tsx

The TokenProvider wraps the RouterProvider to make its Context available to the components rendered by the RouterProvider.

Routing


import { createBrowserRouter } from "react-router-dom";
import App from "./App";
import Callback, { loader as callbackLoader } from "./auth/Callback";
import Claims from "./Claims";
import Root from "./Root";

export const router = createBrowserRouter([
  {
    path: '/',
    element: <Root />,
    children: [
      {
        index: true,
        element: <App />
      },
      {
        path: 'callback',
        element: <Callback />,
        loader: callbackLoader
      },
      {
        path: 'claims',
        element: <Claims />
      }
    ]
  }
]);
src/router.tsx

The Callback component handles the auth response so its path must match the redirect_uri.


import { Outlet } from "react-router-dom";
import Nav from "./Nav";

export default function Root() {
  return (
    <>
      <Nav />
      <Outlet />
    </>
  );
}
src/Root.tsx

import { NavLink } from "react-router-dom";
import styles from './Nav.module.css';

export default function Nav() {
  return (
    <ul className={styles.nav}>
      <li>
        <NavLink 
          to={'/'} 
          className={({isActive}) => isActive ? styles.active : ''}
        >
          App
        </NavLink>
      </li>
      <li>
        <NavLink 
          to={'/claims'} 
          className={({isActive}) => isActive ? styles.active : ''}
        >
          Claims
        </NavLink>
      </li>
    </ul>
  );
}
src/Nav.tsx

Login and Logout


import './App.css';
import { sendAuthReq } from './auth/oidc';
import { useSetToken, useToken } from './auth/TokenProvider';

export default function App() {
  const token = useToken();
  const setToken = useSetToken();

  function login() {
    sendAuthReq();
  }

  function logout() {
    setToken(null);
  }

  return (
    <div>
      {token ? (
        <button onClick={logout}>Logout</button>
      ) : (
        <button onClick={login}>Login</button>
      )}
    </div>
  );
}
src/App.tsx

The user is logged in if the token is not null. To login, call sendAuthReq from the login button's click handler. To logout, set the token to null from the logout button's click handler.

Callback component


import { useEffect } from "react";
import { useLoaderData, useNavigate } from "react-router-dom";
import { handleAuthResp } from "./oidc";
import { useSetToken } from "./TokenProvider";

export default function Callback() {
  const {id_token}: any = useLoaderData();
  const setToken = useSetToken();
  const navigate = useNavigate();

  useEffect(() => {
    setToken(id_token);
    navigate(localStorage.getItem('location') || '/');
  }, [id_token]);

  return null;
}

export function loader() {
  const id_token = handleAuthResp();
  return {id_token};
}
src/auth/Callback.tsx

Because handleAuthResp contains side effects (window.location, localStorage), it has to be called from the Callback's loader instead of its render function. In the Effect, the token state is set to the id_token from the auth response and the app navigates to the location before authentication.

Claims component


import { decodeJwt } from "jose";
import { useToken } from "./auth/TokenProvider";

export default function Claims() {
  const token = useToken();

  let claims: any = {};
  
  if (token) {
    claims = decodeJwt(token);
  }

  return (
    <>
      {token ? (
        <>
          <table border={1}>
            <thead>
              <tr>
                <th>Claim</th>
                <th>Value</th>
              </tr>
            </thead>
            <tbody>
              {Object.entries(claims).map(([name, value]: any) => (
                <tr key={name}>
                  <td>{name}</td>
                  <td>{value}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </>
      ) : (
        <p>No token</p>
      )}
    </>
  );
}
src/Claims.tsx

The useToken hook is used to get the token. The decodeJwt function from the jose module is used to get the token's claims.

Demo!

Redux: My Initial Thoughts

When building non trivial apps with more than a few components needing access to shared state, I like to use a state management framework to keep my state and state update logic in a central and standardized location ourside of any component. This allows my components to directly access state (see right) instead of having to use chains of input params and callbacks to read and update state (see left).

In Angular, my preferred state management framework is NgRx. When searching for a similar framework to use with React, I initially looked at React's built in Reducer, but it lacked a mechanism to handle side effects similar to Effects in NgRx, so I ended up choosing Redux. In this post I will share my initial thoughts regarding Redux from the perspective of a NgRx user after reading the official docs and refactoring my Timesheet app from my previous post to use Redux.

Setup

Project setup was very easy using the create-react-app with the redux-typescript template. It installed Redux Toolkit which provides some abstractions and helpers to make it easier to work with Redux. The included sample Counter feature was helpful as I used it as a reference for implementing my own features.

Reducers

Creating Reducers in Redux was similar to NgRx. Each feature has its own Reducer, referred to as a Slice in Redux, which encapsulates the state, actions, and state update logic for each feature of my app. I liked how Redux is able to infer the Actions and its payload from the method signatures of my Reducer functions so I do not have to manaually define them like in NgRx.

Side Effects

NgRx and Redux differs the most in how they implement side effects such as fetching data from the server. In NgRx, the Effect function containing the side effect logic and the Action which triggers it are separate entities, while in Redux, both concerns are combined into a single entity in the form of a Thunk function. In my opinion, NgRx's implementation is a bit more intuative because it allows me to think of everything in terms of Actions instead of Actions or Thunks, although the tradeoff for decoupling the Action and Effect in NgRx is writting a bit more code. I also think the learning curve for NgRx is higher because it uses RxJs to write Effects, although this might not be an issue for Angular developers.

Selectors

Selectors are implemented similarly in Redux and NgRx.

Final Thoughts

Learning Redux seemed daunting at first given the amount of documentation available on it, but I was able to get up speed pretty quickly by just going through the tutorial from official docs, perhaps because I've already used NgRx. Overall, I like the framework and will be using it in my future React projects.

My Journey to React and Initial Reactions

React is one of the most popular front end web frameworks and is something I've wanted to learn for a while now. I recently had some down time and decided to finally learn it. I started my journey to React about two months ago with the goal of learning core React and building a simple app to validate my knowledge. In this post, I will describe my journey and initial reactions from the perspective of an Angular developer.

My first task was to determine how to learn React. After some preliminary research, I discovered that there were two ways to build components in React; the class method or the functional method. The former is the older more established way of building React components. The latter method is relatively new, but appears to be the preferred method going forward. I chose to learn the functional method. There are not many books on building React apps using functional components to chose from. I picked Learning React from O’Reilly. The book started off good, but I kind of got lost about half way through. I decided to switch to the official React beta documentation, which turned out to be excellent! Even though it was beta, it appeared mostly complete. The challenges at the end of each section were also very helpful for applying the concepts in practice.

For my first React app, I chose to re implement a timecard app I built in Angular a few years ago, with a small enhancement. I originally built the app to help me track my time on various hourly jobs. The app allows me to punch in and out of jobs and has a cool feature which displays updated totals for time and money every second. This initial version used the browser’s localstorage for data storage. For the React version, I plan to use IndexedDB, which is a key/value style NoSQL database inside the browser.

Project setup was very easy with Create React App. This tool generated my project directory with a placeholder App component and start and build scripts. It is similar to running ng new in Angular CLI. I wanted to use TypeScript in my React project so I added the typescript template parameter when running the create-react-app command. I was able to open the project in my VS Code editor and use syntax highlighting and auto completion without additional plugins.

React feels simpler, less verbose, a bit more low level and less abstracted than Angular. There is no two way data binding, forms abstractions, or dependency injection. Each component is defined using a plain JavaScript function encapsulating state and event handlers and returns a view written in a HTML like syntax called JSX. The life cycle of a component is also very simple; whenever the state changes, the view re renders itself. Side effects like making HTTP requests or invoking browser APIs cannot occur during the rendering. They can only occur in event handlers or special functions called effects which can be configured to run on component load or when any subset of the component’s state changes. React does include built in support for reducers to centralize the management of application state and state changes, but I did not find them very useful because they lack support for triggering side effects in response to actions like in NgRx effects. React does have a very useful third party component library similar to Angular Material called MUI which I used to make the app’s UI look nice.

I finished building the app in about two weeks and deployed it using Azure's Static Website service. The app is live here.

I enjoyed building my first React app and am happy to add React to my front end development toolbox. I plan to use it in future projects.

Intro to Machine Learning using Transformers

A few months ago, I began to learn about machine learning and AI. Initially, I was expecting a steep learning curve with a lot of complex...