GeistHaus
log in · sign up

https://tania.dev/rss.xml

rss canonical for stream
120 posts
Polling state
Status active
Last polled May 19, 2026 04:49 UTC
Next poll May 20, 2026 06:04 UTC
Poll interval 86400s
ETag "f43da610ba0ec1da6fa46eea6728e7db-ssl-df"

Posts

Enabling Apache ECharts in React for Data Visualization
Making dashboards with charts and graphs is a pretty common part of the front-end developer experience, as well as deciding which JavaScript…
Show full content

Making dashboards with charts and graphs is a pretty common part of the front-end developer experience, as well as deciding which JavaScript library to use for data visualization. As you explore all the different options - Recharts, Visx, D3 - you'll see they all do something you want, and lack something you need.

For a while I was using Nivo, a really impressive React component library, but it didn't have some features I needed like drilling down from one graph type into another, so I started looking into Apache ECharts.

This library is open-source, awesome, and incredibly easy to use. However, as of writing, the React component wrapper (echarts-for-react) that someone made is pretty out of date - last updated more than three years ago, and relies on an old version of ECharts that didn't have all the features I was looking for, so I made a tiny little wrapper that can just be imported as a component in a single file..

Demo

ECharts works by creating an option object that holds all the configuration about the graph. That makes it incredibly easy to work with, because it doesn't require separate components for each graph type.

App.js
import { EChart } from './EChart'

export const App = () => {
  const option = {
    // ... chart configuration goes here
  }

  return <EChart option={option} />
}

Here's a little demo importing the default Line, Bar and Pie type charts.

Installation

Just install the echarts dependency and you're good to go.

npm i echarts
Using ECharts

With just a few methods from the API, you can get this up and running.

ECharts

First you'll need to create a div and use that ref to inject the ECharts instance, then you can initialize the echartInstance with init(). For as long as it exists, you can access the instanced with getInstanceByDom(). When you're done, you can disconnect() from the instance.

Function Description init Creates an ECharts instance getInstanceByDom Returns chart instance of dom container disconnect Disconnects interaction of multiple chart series. EChart Instance

Once you have an echartsInstance in the DOM, you can use setOption to update the configuration. We'll be able to update this with useEffect in React. When you're done, you can destroy the instance with dispose()

Function Description setOption Configuration item, data, universal interface, all parameters and data can all be modified through setOption dispose Destroys chart instance, after which the instance cannot be used any more <EChart> Component

To set up the basic Echart component, you can create a ref on an element and initialize the chart on mount using init(). All the configuration of echarts is set in setOption, so whatever option you pass into the component will initialize the chart.

Everything you need to get set up can be handled in these two useEffects.

EChart.js
import React, { useMemo, useRef, useEffect } from 'react'
import { init, getInstanceByDom } from 'echarts'

export const EChart = ({
  option,
  chartSettings,
  optionSettings,
  style = { width: '100%', height: '350px' },
  ...props
}) => {
  const chartRef = useRef(null)

  useEffect(() => {
    // Initialize chart
    const chart = init(chartRef.current, null, chartSettings)

    return () => {
      chart?.dispose()
    }
  }, [])

  useEffect(() => {
    // Re-render chart when option changes
    const chart = getInstanceByDom(chartRef.current)

    chart.setOption(option, optionSettings)
  }, [option])

  return <div ref={chartRef} style={style} {...props} />
}

Now you'll have an element that contains the chart and re-renders if the options change.

Events

The component is pretty basic right now, and there's a few things you'll probably want to add. You can create an event handler to pass a click event into the chart like so:

<EChart
  option={option}
  events={{
    click: () => {
      console.log('Click handler!')
    },
  }}
/>

This enables any type of event and handler you want to add.

import React, { useMemo, useRef, useEffect } from 'react'
import { init, getInstanceByDom } from 'echarts'

export const EChart = ({
  option,
  chartSettings
  optionSettings
  style = { width: '100%', height: '350px' },
  events = {},  ...props
}) => {
  const chartRef = useRef(null)

  useEffect(() => {
    // Initialize chart
    const chart = init(chartRef.current, null, chartSettings)

    // Set up event listeners    for (const [key, handler] of Object.entries(events)) {      chart.on(key, (param) => {        handler(param)      })    }
    // Return cleanup function
    return () => {
      chart?.dispose()
    }
  }, [])

  useEffect(() => {
    // Re-render chart when option changes
    const chart = getInstanceByDom(chartRef.current)

    chart.setOption(option, optionSettings)
  }, [option])

  return <div ref={chartRef} style={style} {...props} />
}
Resizing

You also might want the chart to resize if the screen changes, or if the layout changes, such as opening/closing a sidebar.

I created a debounced event with Lodash debounce() and useMemo(), which will be debounced 50 milliseconds. This will help performance so the chart doesn't constant run resize() during a resize event, but attempts to do it only once.

const resizeChart = useMemo(
  () =>
    debounce(() => {
      if (chartRef.current) {
        const chart = getInstanceByDom(chartRef.current)
        chart.resize()
      }
    }, 50),
  []
)

I used the ResizeObserver() WebAPI to determine when to resize in the useEffect.

const resizeObserver = new ResizeObserver(() => {
  resizeChart()
})

resizeObserver.observe(chartRef.current)

Now the EChart component is complete - it will resize on window or layout change, pass events through, and update the graph based on the option configuration passed in.

EChart.js
import React, { useMemo, useRef, useEffect } from 'react'
import { init, getInstanceByDom } from 'echarts'
import { debounce } from 'lodash'
export const EChart = ({
  option,
  chartSettings,
  optionSettings,
  style = { width: '100%', height: '350px' },
  events = {},
  ...props
}) => {
  const chartRef = useRef(null)

  // Debounce resize event so it only fires periodically instead of constantly  const resizeChart = useMemo(    () =>      debounce(() => {        if (chartRef.current) {          const chart = getInstanceByDom(chartRef.current)          chart.resize()        }      }, 50),    []  )
  useEffect(() => {
    // Initialize chart
    const chart = init(chartRef.current, null, chartSettings)

    // Set up event listeners
    for (const [key, handler] of Object.entries(events)) {
      chart.on(key, (param) => {
        handler(param)
      })
    }

    // Resize event listener    const resizeObserver = new ResizeObserver(() => {      resizeChart()    })    resizeObserver.observe(chartRef.current)
    // Return cleanup function
    return () => {
      chart?.dispose()

      if (chartRef.current) {        resizeObserver.unobserve(chartRef.current)      }      resizeObserver.disconnect()    }
  }, [])

  useEffect(() => {
    // Re-render chart when option changes
    const chart = getInstanceByDom(chartRef.current)

    chart.setOption(option, optionSettings)
  }, [option])

  return <div ref={chartRef} style={style} {...props} />
}
Conclusion

Setting up the EChart component was a good exercise in creating my own simple wrapper for a JavaScript library without relying on the third-party React implementation. Supposedly, other frameworks like Svelte make it a lot easier to implement third party JavaScript libraries without making them "React-specific, but I work primarily in React, so it's a useful skill to have.

You can view the Demo and source code on CodeSandbox, which uses the completed EChart component to display a line, bar, and pie chart.

https://taniarascia.com/apache-echarts-react/
Year in Review: 2024 into 2025
I'm trying to win the award for "Latest Year in Review". 🏆 I don't know if anyone can challenge me at this point, as it's almost thirdway…
Show full content

I'm trying to win the award for "Latest Year in Review". 🏆 I don't know if anyone can challenge me at this point, as it's almost thirdway through the year. But, I've done this in some way, shape, or form for the last eight years now!

Coding

I still write code! Here's my contribution history from 2024 for work:

2024 contributions w

And my contributions to personal projects and this website over the same period:

2024 contributions p

I've been at my current job for four years now, with two of them as a principal engineer. I spend my time solving problems and writing React and CSS. I have the luxury of writing basically any component I need from scratch and creating a custom component system.

After over a decade of working eight hours a day, it's become harder and harder to spend any time after hours working on side projects or writing for the blog. I enjoy my work and I think it's the best career for me, but keeping my coding time to the morning and afternoon hours prevents burnout for me. I still want to write about interesting things I've discovered here and there, but it's hard to find the time.

I also don't keep up with what's going on in "The JavaScript World" anymore. I rarely remember I have a Bluesky account just because I'm not too into scrolling, and when I do scroll I gravitate to reddit.

Writing

I wrote a few things this year:

  • Tables with Fixed Headers and Horizontal Scroll - A surprisingly difficult problem to tackle. I made a full-page datagrid/table component with sorting and filtering of various datatypes, pagination, bulk select, reordering, and fixed headers. I documented what method I went with for the fixed headers here.
  • Creating a Keyboard Shortcut Hook in React (Deep Dive) - Everything in React is hooks, so I made a useShortcut hook for some shortcut interactions I built. For fun, I used the Konami code in the example sandbox and made the page look all retro and eighties if you press the key combination.
  • Redesign: Version 7.0: Sidebars, light-dark, and Bluesky - I might not always be able to inspire myself to write about code, but I still enjoy tweaking the blog layout and trying to make it look perfect. I'm pretty happy with the current design - the code is really easy to maintain (just one style.css file with light-dark() variables), the table of contents gives you a sneak peak at the content of the articles, and the sidebar highlights some articles I've put a lot of time into.

My goal is to write more small snippets of articles about some epiphany I had or some small problem I solved. I feel like those can be helpful and shouldn't feel like too big of a project to embark on. I'm not very interested in writing complete start-to-finish tutorials anymore, as they're extremely time-consuming, probably feel overwhelming to even read, and much of the preamble feels repetitive.

I'd also like to write some posts this year about more components I've written from scratch, like a dropdown, matrix chart, and tooltips.

Gaming and Media

2024 was a big gaming year! The year started off strong with Prince of Persia: The Lost Crown. Castlevania: Symphony of the Night is one of my favorite games of all time, as well as Hollow Knight, so I'm a big fan of Metroidvanias. Prince of Persia did it really well.

Final Fantasy VII: Rebirth was another game I played that came out in 2024, and I was incredibly impressed by it. I'm part of the initial wave of Final Fantasy fans, having played FF7 as an eight-year-old the first time around. (Although I'm one of those obnoxious people who will go on about how FF6 is actually the best one.) Rebirth was great, and we put at least 100 hours into it, doing all the side quests and everything. It's the first JRPG I've played in a long, long time and it was really fun to play one again.

At the tail end of 2024, Marvel Rivals came out, and that's been one of my main hobbies since it came out. It's great to just hop on and be able to jump into a game with a few people and try to climb the ranks. I mostly play strategists and vanguards (healers and tanks) and am working my way through Diamond right now.

Aside from gaming, my favorite show that I discovered this year is Severance. If you haven't seen it, I highly recommend it! It's having a big cultural moment right now, and I'm happy to see something so good get so popular.

Personal

Life is good for me. I really enjoy my job, I share my life with a wonderful partner and soon-to-be husband, and we have two adorable kitties. I'm just living my life, doing laundry and taxes. I'm also a homeowner as of this year, after over a decade of moving and paying rent, which is awesome. Otherwise, I spend it working, gaming, keeping up with friends, doing projects around the house, and cooking all sorts of things.

I hope 2025 is good to you!

https://taniarascia.com/2024-into-2025/
Redesign: Version 7.0: Sidebars, light-dark, and Bluesky
One of my favorite hobbies is tweaking my website. I've updated the design countless times thoughout the decade (!) of this site's existence…
Show full content

One of my favorite hobbies is tweaking my website. I've updated the design countless times thoughout the decade (!) of this site's existence. Here's a small history of the changes:

Redesign History
  • First was version 2.0, a dozen or so layouts I made when this was a WordPress site.
  • Then was version 3.0 and 4.0, where I wavered back and forth between busy and minimalist layout.
  • I migrated from WordPress to Gatsby at some point.
  • Then I decided to make the site look like Visual Studio Code with some custom pixel art and a theme color selector in version 5.0. This was probably my favorite version.
  • Once again, I felt like the site was too busy, so I simplified it for version 6.0.
Version 7.0

I had a few days off for Thanksgiving this week, so in-between cooking, prepping, and cleaning for a few events we were hosting, I managed to redesign the site. I'm really happy with how it turned out! It was inspired by Docusaurus.

Organization

I finally settled on a good way to separate my technical articles from personal articles, by having Notes for personal writings and Articles for tutorials, guides, and everything programming related. I also have my open-source Projects highlighted, and even some art. On the front page, I have an "In depth" section on the main page to highlight my long-form articles.

I also decided to add some sidebars to make it easier to explore the site.

Sidebars

In the main sidebar, I picked a few articles I really like and put them in a "Favorites" section, so people can hopefully click around and discover some really cool guides, like this CSS manifesto and this article about how to organize a React app's directory structure, and my ode to Animorphs.

I also have a secondary sidebar on the article pages with the date, tags, and table of contents. It uses the IntersectionObserver API to detect when each section scrolls into view.

Bluesky

The biggest fad right now in developer-based social media is Bluesky, an alternative Twitter clone. I'm moving over to Blueksy in the hopes of interacting with real people again. I made a 🦋 Bluesky Starter Pack of developers I think are pretty cool. A starter pack is a way to quickly add a group of people on the app. If you're not using Bluesky yet, you can click the link to get started. I plan to keep adding to it as I discover new and old friends on the app.

Styling

The entire style of this page was written in one style.css file. I'm not a fan of Tailwind or CSS-in-JSS-type solutions, so I just write plain CSS from scratch. Now that CSS has variables and nesting, there's no need to set up CSS preprocessors anymore.

Making a light and dark theme switch is easier than ever, and I decided to take advantage of the latest CSS spec for that.

On the root pseudo class, you can now use the color-scheme property, and set it to light dark.

style.css
:root {
  color-scheme: light dark;
}

This enables the light-dark() function, which allows you to set two values, which will apply to light and dark mode, respectively.

style.css
:root {
  --color-background-navbar: light-dark(var(--gray-05), var(--gray-10));
  --color-background-sidebar: light-dark(var(--gray-05), var(--gray-10));
  --color-background-code: light-dark(var(--gray-05), var(--gray-10));
}

All I need to do to switch between the themes is update the color-scheme property.

Layout.js
export const Layout = () => {
  const [theme, setTheme] = useState('dark')

  const handleUpdateTheme = (newTheme) => {
    window.localStorage.setItem('theme', newTheme)
    document.documentElement.style.setProperty('color-scheme', newTheme)

    setTheme(newTheme)
  }
}

The process to have a light/dark mode has evolved a lot over the past few years. First you had to make a separate .dark class and manually update all the colors. Once CSS variables and :root came around, you could just make one set of variables for light and dark. Now, all you need to do is use light-dark and set the color scheme property. I'm glad it's so easy to take advantage of the latest CSS.

New Layout Main Page

7 index

Blog Post

7 article

Light theme

7 article light

Old Layout Main Page

6 index

Blog Post

6 article

Light theme

6 article light

Conclusion

I hope you like the new layout! Let me know what you think. I hope that this layout makes it easier to navigate around the site and explore all the content that was previously hidden behind minimalism.

https://taniarascia.com/redesign-version-7/
Creating a Keyboard Shortcut Hook in React (Deep Dive)
Recently I needed to add some keyboard shortcuts to an app I was working on. I wrote up some example code and decided to write this article…
Show full content

Recently I needed to add some keyboard shortcuts to an app I was working on. I wrote up some example code and decided to write this article about it. It goes into various types of shortcuts, caching, some potential bugs and pitfalls you might encounter, and more. 👇

Goals

I made a custom React hook to handle keyboard shortcuts. Here are the links to the demo and source code:

It can handle the following combinations of key presses:

  • A single keypress: X
  • A combination keypress with modifiers: ⌥ + X (Option + X)
  • A combination keypress with multiple modifiers: ⌘ + ⇧ + X (Command + Shift + X)
  • A sequence of characters: ↑ ↑ ↓ ↓ ← → ← → B A

It also makes sure:

  • The shortcut will cache the event handler function if nothing changed (with useCallback)
  • The shortcut always uses the latest function value, not holding on to stale state (with useLayoutEffect)
  • Shortcuts won't run while you're typing in a text input, textarea, or contenteditable element (unless you want them to)

You'll be able to run the hook in a component like this:

useShortcut('Command+Shift+X', () => console.log('¡Hola, mundo!'))

I'll make a little example to show everything working in a minimalist fashion, or you can just go to the sandbox or demo and play around with it.

Try any of those key combinations, including but not limited to the Konami Code!

Note: The Konami Code is an old cheat code for video games that has also been included as a secret in a lot of websites.

Setting Up

I don't usually like using a contrived example like count, setCount and just increasing state, but in this case we just care about making sure the shortcut is working and accessing state properly, so I decided to make it simple. Here's a simple React app with a button that increments and displays the count, and an input.

App.js
import { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <button type="button" onClick={() => setCount((prev) => prev + 1)}>
        {`Count: ${count}`}
      </button>
      <button type="button" onClick={() => setCount(0)}>
        Clear
      </button>
      <input type="text" placeholder="Type a shortcut key" />
    </div>
  )
}

The front end buttons aren't overly necessary, they just help with debugging. The input will come in handy later.

useShortcut Hook

We're going to want to be able to implement the useShortcut hook like this:

App.js
import { useState } from 'react'
import { useShortcut } from './useShortcut'

export default function App() {
  const [count, setCount] = useState(0)

  useShortcut('a', () => {    setCount((prev) => prev + 1)  })
  return <div>{/* ... */}</div>
}

So let's make the simplest version of the hook. You'll make a useShortcut function that takes two parameters:

  • shortcut - the string representing a key, key combination, or key sequence (currently just a key)
  • callback - the function that should be called when the correct key is pressed

It consists of a handleKeyDown function that checks if the correct key is being pressed by comparing shortcut to event.key, and runs the passed in function if so. A useEffect adds the event listener on the keydown event, and removes it if dismounted. There's no dependency array in the useEffect, so it will always fire.

useShortcut.js
import { useEffect } from 'react'

export const useShortcut = (shortcut, callback) => {
  const handleKeyDown = (event) => {
    // Single key shortcuts (e.g. pressing a)
    if (shortcut === event.key) {
      return callback(event)
    }
  }

  useEffect(() => {
    window.addEventListener('keydown', handleKeyDown)

    return () => {
      window.removeEventListener('keydown', handleKeyDown)
    }
  })
}

Now if you press A, the count will increment.

Performance Optimization

This will work every time, because the useEffect has no dependency array, and fires on every render. This is pretty inefficient, though. If I go to the Performance Monitor in Chrome DevTools (Command + Shfit + P in DevTools, and search for "Performance Monitor") I can see 20 or more event listeners being added with every time the shortcut is run.

In an attempt to reduce how often this is firing, I can use useCallback on the handleKeyDown function and pass it into the useEffect array.

Note: Going forward, React shouldn't require so much finagling with useCallback and useMemo, but since you won't always have the option to use the latest and greatest, it's good to know how to work with this.

useShortcut.js
import { useCallback, useEffect } from 'react'
export const useShortcut = (shortcut, callback) => {
  const handleKeyDown = useCallback((event) => {    // Single key shortcuts (e.g. pressing a)
    if (shortcut === event.key) {
      return callback(event)
    }
  }, [])
  useEffect(() => {
    window.addEventListener('keydown', handleKeyDown)

    return () => {
      window.removeEventListener('keydown', handleKeyDown)
    }
  }, [handleKeyDown])}

I've wrapped handleKeyDown in useCallback in an attempt to memoize the function, and passed that memoized function into the dependency array of the useEffect. I can see in the Performance Monitor that this only adds one event listener per keydown event now instead of 30.

Holding Onto Stale State

This seems good at first - the shortcut is still working, and the Performance Monitor is looking better. However, this works because the setCount function is using a callback as the value ( prev + 1)), instead of updating the value directly (setCount(count + 1)) ensuring the previous count value is always up to date.

In the real world, we can't always guarantee that every bit of state we're working with is a setState callback. If I make a shortcut that accesses the direct state value of count and attempts to modify it, like so:

useShortcut('w', () => {
  setCount(count + count)
})

The W shortcut will not be guaranteed to be accessing the latest count state. You can see what happens in the below example if you press A a few times to increase the count, then press W to add count + count

If you increase the count to something like 3, you'd expect running the W shortcut to return 6. However, what you'll end up with is 0.

That's because the initial state of count gets passed into the function, and there's nothing in the useCallback dependency array, so it never updates with the new value.

Accessing Current State

One way to solve this is to ensure that the function passed in is always wrapped in a useCallback.

const handleW = useCallback(() => setCount(count + count), [count])

useShortcut('w', handleW)

Then add callback to the dependency array of handleKeyDown in useShotcut. However, I'm not a fan of that approach because it requires that the user to always remember to memoize their function and pass in any state that might update. It should be easier to less prone to potential bugs to use a hook.

I discovered this interesting pattern to create a ref to hold the callback, and use useLayoutEffect to to update the ref.

I've used useLayoutEffect in the past for scrolling purposes, (for example, to make sure a reload of a page always starts at the top) but this is the first time I've seen it use in this way with refs.

useShortcut.js
import { useRef, useLayoutEffect, useCallback, useEffect } from 'react'
export const useShortcut = (shortcut, callback) => {
  const callbackRef = useRef(callback)  useLayoutEffect(() => {    callbackRef.current = callback  })
  const handleKeyDown = useCallback((event) => {
    if (shortcut === event.key) {
      return callbackRef.current(event)    }
  }, [])

  useEffect(() => {
    window.addEventListener('keydown', handleKeyDown)

    return () => {
      window.removeEventListener('keydown', handleKeyDown)
    }
  }, [handleKeyDown])
}

Using that code, I can see that it uses the correct and most up to date count state without forcing the consumer of the useShortcut hook to pass the callback in with useEffect. I can also see in the Performance Monitor that it's not adding tons of event listeners the way the original code was. I'd be interested to hear more opinions on this pattern, because it does seem to add a lot of boilerplate, but it does seem to make the hook more optimized and easier to use.

Now that everything is working properly for a single keypress, we can move on to adding a combination shortcuts.

Combination Keyboard Shortcuts with Modifiers

So far, using if (shortcut === event.key), we can make sure the key being pressed matches the shortcut and run it accordingly. But keyboard shortcuts are usually combinations, using the modifiers at the bottom left and right of the keyboard. There are four, and they each have an event property associated with them being pressed:

  • ^ Control - event.ctrlKey
  • Option or Alt - event.altKey
  • Command or Windows - event.metaKey
  • Shift - event.shiftKey

If you're pressing Shift, event.shiftKey will return true.

We want to enable the use of one or multiple modifiers with a key, like so:

useShortcut('Control+C', () => {
  setCount(count + count)
})

useShortcut('Command+Shift+X', () => {
  setCount(count + count)
})

I decided to solve this by making an Object there the string of the modifier is mapped to the modifier event. If your shortcut has a +, and any of those modifiers are pressed along with the key, run the shortcut.

useShortcut.js
import { useRef, useLayoutEffect, useCallback, useEffect } from 'react'

export const useShortcut = (shortcut, callback) => {
  const callbackRef = useRef(callback)

  useLayoutEffect(() => {
    callbackRef.current = callback
  })

  const handleKeyDown = useCallback((event) => {
    const modifierMap = {      Control: event.ctrlKey,      Alt: event.altKey,      Command: event.metaKey,      Shift: event.shiftKey,    }
    if (shortcut.includes('+')) {      const keyArray = shortcut.split('+')      // If the first key is a modifier, handle combinations      if (Object.keys(modifierMap).includes(keyArray[0])) {        const finalKey = keyArray.pop()        // Run handler if the modifier(s) + key have both been pressed        if (keyArray.every((k) => modifierMap[k]) && finalKey === event.key) {          return callbackRef.current(event)        }      }    }
    if (shortcut === event.key) {
      return callbackRef.current(event)
    }
  }, [])

  // ...
}

Now pressing one or more modifiers along with the key will run the shortcut.

Note: There are some issues with the Alt key, which I discuss in the conclusion.

Shortcuts and Text Inputs

Something that will come up while creating shortcuts in the real world is deciding when and where you want them to be available. If you have a shortcut that saves the current page you're working on, maybe by pressing S, you don't want that shortcut to run while the user is typing into a text box.

I've added an options property to the useShortcut hook, with a default setting of disableTextInputs: true. If the shortcut you're creating is explicitly for use while typing, you can disable it.

I've disabled it for HTMLTextAreaElement, HTMLInputElement where type = text, and contenteditable elements.

useShortcut.js
export const useShortcut = (
  shortcut,
  callback,
  options = { disableTextInputs: true }) => {
  const callbackRef = useRef(callback)

  useLayoutEffect(() => {
    callbackRef.current = callback
  })

  const handleKeyDown = useCallback((event) => {
    const isTextInput =      event.target instanceof HTMLTextAreaElement ||      (event.target instanceof HTMLInputElement &&        (!event.target.type || event.target.type === 'text')) ||      event.target.isContentEditable
    // Don't enable shortcuts in inputs unless explicitly declared    if (options.disableTextInputs && isTextInput) {      return event.stopPropagation()    }
    // ...
  }, [])

  // ...
}

There are other ways to handle this, like checking if tagName is "INPUT", but I prefer ensuring it's a text-type input, because you might have a shortcut that works with other types of inputs, so I think this is a good solution.

Key Sequences

The last thing I want to handle is a sequence of characters, such as A + B + C all pressed in succession.

For my example, I used the Konami Code, which is "up up down down left right left right b a" pressed in succession.

const handleKonamiCode = () => {
  /* ... */
}

useShortcut(
  `ArrowUp+ArrowUp+
ArrowDown+ArrowDown+
ArrowLeft+ArrowRight+
ArrowLeft+ArrowRight+
b+a`,
  handleKonamiCode
)

In order to set this up, I'm going to create some state to hold onto any matching combination of keys, called keyCombo. After splitting the shortcut string by + and putting it into an array, you can just keep adding each matching key to the keyCombo array. If it's the last one in the sequence, run the callback. If it doesn't match the sequence, clear the queue.

useShortcut.js
import {
  useCallback,
  useRef,
  useLayoutEffect,
  useState,  useEffect,
} from 'react'

export const useShortcut = (
  shortcut,
  callback,
  options = { disableTextInputs: true }
) => {
  const callbackRef = useRef(callback)
  const [keyCombo, setKeyCombo] = useState([])
  // ...

  const handleKeyDown = useCallback(
    (event) => {
      // ...

      // Handle combined modifier key shortcuts (e.g. pressing Control + D)
      if (shortcut.includes('+')) {
        const keyArray = shortcut.split('+')

        // If the first key is a modifier, handle combinations
        if (Object.keys(modifierMap).includes(keyArray[0])) {
          const finalKey = keyArray.pop()

          // Run handler if the modifier(s) + key have both been pressed
          if (keyArray.every((k) => modifierMap[k]) && finalKey === event.key) {
            return callbackRef.current(event)
          }
        } else {
          // If the shortcut doesn't begin with a modifier, it's a sequence          if (keyArray[keyCombo.length] === event.key) {            // Handle final key in the sequence            if (              keyArray[keyArray.length - 1] === event.key &&              keyCombo.length === keyArray.length - 1            ) {              // Run handler if the sequence is complete, then reset it              callbackRef.current(event)              return setKeyCombo([])            }            // Add to the sequence            return setKeyCombo((prevCombo) => [...prevCombo, event.key])          }          if (keyCombo.length > 0) {            // Reset key combo if it doesn't match the sequence            return setKeyCombo([])          }        }
      }

      // ...
    },
    [keyCombo.length]  )

 // ...
  }, [handleKeyDown])
}

I also added the keyCombo length to the dependency array of handleKeyPress since the function depends on it. Pressing a combination of keys will run the shortcut now.

Conclusion

Here is our completed useShortcut hook: (I also added a line to ignore if event.repeat is true, meaning a key is just being held down)

useShortcut.js
import { useCallback, useRef, useLayoutEffect, useState, useEffect } from 'react'

export const useShortcut = (shortcut, callback, options = { disableTextInputs: true }) => {
  const callbackRef = useRef(callback)
  const [keyCombo, setKeyCombo] = useState([])

  useLayoutEffect(() => {
    callbackRef.current = callback
  })

  const handleKeyDown = useCallback(
    (event) => {
      const isTextInput =
        event.target instanceof HTMLTextAreaElement ||
        (event.target instanceof HTMLInputElement &&
          (!event.target.type || event.target.type === 'text')) ||
        event.target.isContentEditable

      const modifierMap = {
        Control: event.ctrlKey,
        Alt: event.altKey,
        Command: event.metaKey,
        Shift: event.shiftKey,
      }

      // Cancel shortcut if key is being held down
      if (event.repeat) {
        return null
      }

      // Don't enable shortcuts in inputs unless explicitly declared
      if (options.disableTextInputs && isTextInput) {
        return event.stopPropagation()
      }

      // Handle combined modifier key shortcuts (e.g. pressing Control + D)
      if (shortcut.includes('+')) {
        const keyArray = shortcut.split('+')

        // If the first key is a modifier, handle combinations
        if (Object.keys(modifierMap).includes(keyArray[0])) {
          const finalKey = keyArray.pop()

          // Run handler if the modifier(s) + key have both been pressed
          if (keyArray.every((k) => modifierMap[k]) && finalKey === event.key) {
            return callbackRef.current(event)
          }
        } else {
          // If the shortcut doesn't begin with a modifier, it's a sequence
          if (keyArray[keyCombo.length] === event.key) {
            // Handle final key in the sequence
            if (
              keyArray[keyArray.length - 1] === event.key &&
              keyCombo.length === keyArray.length - 1
            ) {
              // Run handler if the sequence is complete, then reset it
              callbackRef.current(event)
              return setKeyCombo([])
            }

            // Add to the sequence
            return setKeyCombo((prevCombo) => [...prevCombo, event.key])
          }
          if (keyCombo.length > 0) {
            // Reset key combo if it doesn't match the sequence
            return setKeyCombo([])
          }
        }
      }

      // Single key shortcuts (e.g. pressing D)
      if (shortcut === event.key) {
        return callbackRef.current(event)
      }
    },
    [keyCombo.length]
  )

  useEffect(() => {
    window.addEventListener('keydown', handleKeyDown)

    return () => {
      window.removeEventListener('keydown', handleKeyDown)
    }
  }, [handleKeyDown])
}

Once again, you can play around with the demo or the sandbox. There is still more that can be done here - for example, handling an Alt key can be tricky becuause pressing Alt + C will actually produce "ç" (not "C") as both the output and event.key value. Some overlapping shortcuts on the same page might have issues.

Overall, this should give you a good idea of how to work with a custom hook, avoid bugs (like holding onto stale state), improve caching (with useCallback) and set up various types of keyboard events.

Thanks for reading! I'd be happy to hear any additional thoughts about shortcuts and hooks you might have.

https://taniarascia.com/keyboard-shortcut-hook-react/
HTML Tables with Horizontal Scroll and Sticky Headers
Turns out, creating an HTML table that has both horizontal scroll and fixed headers can be a tricky problem. As another developer said about…
Show full content

Turns out, creating an HTML table that has both horizontal scroll and fixed headers can be a tricky problem. As another developer said about this problem, You would think this is easy. But is really isn't.

Horizontal Scroll

When working with a table that has a lot of columns, you can ensure everything remains visible by adding horizontal scroll. You can do this by wrapping the table in a div that has overflow-x set to auto.

Note: I prefer to use overflow-x: auto instead of overflow-x: scroll because scroll will always show the scroll container, even if there is no scroll available. It looks cleaner.

style.css
.table-outer {
  overflow-x: auto;
}
index.html
<div class="table-outer">
  <table>
    <thead>
      /* table columns go here */
    </thead>
    <tbody>
      /* table data goes here... */
    </tbody>
  </table>
</div>

This works great initially, but there can be some problems.

  • If you have a lot of rows, as you scroll down, you can't see which table headers are associated with each cell
  • If your table rows are selectable and you have bulk actions in the header, you won't see them as you scroll down
  • If there is more content on the page below the table, you won't see the headers as you scroll down
Sticky Headers

You might think that adding position: sticky to the thead of the table would ensure that as you scroll down, the headers will remain fixed to the top of the screen until you scroll past the table.

style.css
.table-outer {
  overflow-x: auto;
}

thead {
  position: sticky;
  z-index: 2;
  top: 0;
}

But as you can see in this example, the table headers are not fixed to the top of the screen as you scroll down.

Since you're now using an overflow container, position: sticky will only apply to the overflow container, and if you're not scrolling within it, it will just be ignored.

Solution

The solution was to add a height to the table wrapper. In this example, I chose to make the height 100% of the viewport window, minus 100px to enable the page title and footer to always be visible. This results in a full-screen datagrid type view.

style.css
.table-outer {
  overflow-x: auto;
  height: calc(100vh - 100px); /* full height minus header and footer */
}

header {
  height: 60px;
}

footer {
  height: 40px;
}

As you can see here, the page title and footer are always visible, and the table is both horizontally and vertically scrollable. This table has fixed headers and also the first row (ID) is fixed to the left of the screen as you scroll horizontally.

Conclusion

And that's all! Hopefully this helps anyone that is struggling to figure out a good solution for their scrollable table.

https://taniarascia.com/horizontal-scroll-fixed-headers-table/
Year in Review: 2023 into 2024
It's 2024, y'all. We're 20 days into the year, so I'm running out of time to write that end of year post I always do. I haven't quite known…
Show full content

It's 2024, y'all. We're 20 days into the year, so I'm running out of time to write that end of year post I always do. I haven't quite known what to write, but here goes.

Career

One big thing I realized is that 2024 marks ten years (!) that I've been a developer, which is my second career. In 2014, I interned during the day for a bartender I knew who had a WordPress shop, and worked at night at NAMCO's Pac-Man restaurant (yes, somehow that was a thing. Billy Mitchell even showed up).

By the first half of 2015, I found my first job as a junior web developer for Lettuce Entertain You, a Chicago restaurant group, and I've been in the field ever since! I love my career and I'm happy I've had the opportunity to work in this field and keep doing it for a full decade.

Coding & Writing

I wrote two posts, one about where to set up websockets in a React/Redux application, and my final post for DigitalOcean, which was about the GraphQL type system. Both bangers.

I learned some stuff that I didn't get around to writing, but most of my coding has been on work projects during work hours. I've written about burnout in the past, and I think it really takes a few years to get over it. Writing thirty-five articles for DigitalOcean in particular burned me out, because I had to write in a very dry style that took the joy of writing out of it for me.

I think I'm getting to the point where I'd be excited to start writing again, and mostly I'm more interested non-software related topics. I really enjoyed writing the Ode to Animorphs post, for example. I'd also be interested in writing about weight lifting, Lego, gaming, cooking, art, music, DDR, and other hobbies and interests of mine.

Life

2023 has been a big year.

The most important thing that happened to me was falling in love. It's hard to find the right words to describe how much joy and happiness being with Austin has brought into my life. I feel light and free. I feel content and understood and cherished. I'm no longer alone; my partner is with me, and all the difficult parts of life are just easier. Everything is better with Austin.

Also, I moved away from Chicago for the first time in my life. I've lived in and explored and had many adventures in Chicago's neighborhoods throughout the years...late night pizza at Dimo's, dancing at The Owl or Beauty Bar or Berlin, playing volleyball on the beach, getting tacos in Little Village...Chicago will always be my hometown, but my home is a person, not a place, and it's been very exciting to get to know my new city!

There have been personal emergencies and trauma this year which have been extremely difficult, but fortunately it has been stabilizing and I'm hoping for a peaceful 2024.

Gaming

I've played a lot of video games this year, more than I have in a long time, which has been great. We're currently playing the new Prince of Persia game, which takes all the gameplay elements I love about Castlevania: Symphony of the Night - one of my all time favorite games - and updates it to be way more interesting, fun, and smooth. I also picked up Hollow Knight and got 97% of the way through it...I just need to beat the final boss!

The game of the year for me, and maybe even for you, was Baldur's Gate 3. I played first as a dwarf druid and then as a half-orc barbarian. With almost two complete playthroughs, we put at least 200 hours into the game, and I'm still looking forward to another run.

I also discovered that I like roguelikes a lot, and I played a lot of Deadcells, Slay the Spire, and Vampire Survivors. I'm excited to catch up on at least a decade of classics that I've never played.

Lifting

I'm getting back into weight lifting, and have been going every Monday, Wednesday, and Friday in the mornings since the beginning of the year. Consistency has always been the most difficult part for me, but it's been three weeks of going every day as planned and I'm excited to see what progress I can make. Here are my current numbers, I hope they're a little bigger in my next end of year review!

  • Squats: 95lbs - 5x5
  • Deadlifts: 115lbs - 5x5
  • Bent Over Row: 85lbs - 5x5
  • Overhead Press: 40lbs - 5x5
  • Bench Press: 65lbs - 5x5
Internet

One thing that has made it hard to inspire myself to write here is that the internet is just...different now. It used to be called "Surfing the 'Net" and you never knew what you were going to find: a fansite, a personal art gallery, or a place where someone documented everything they knew about some obscure topic. I've intended this site to be a bit like the fun old websites of yore.

Now I scroll through reddit a bit and check the news, whether hacker or standard, but aside from a few small Slack or Discord spots, I don't spend too much time on the web. It's harder and harder to find genuine, interesting people and sites instead of ads and bots, and I think the ease at which we can create AI generated content isn't going to make that much better.

Nonetheless, the internet has been part of my life ever since my brother took me to the library when I was eight, and even though it's changed a lot since then, there are still cool things to stumble upon, and I want my website to be one of them.

In conclusion

2023 has been the best year of my life, even though some things have been unbearably difficult. My new life and partnership has made me happier than I can express. I hope you all have a great 2024 and I'll see you around.

us

https://taniarascia.com/2023-into-2024/
How to Use WebSockets in a Redux Application
At some point, you might work on a React/Redux application that requires the use of WebSockets, such as for chat or live updates on a…
Show full content

At some point, you might work on a React/Redux application that requires the use of WebSockets, such as for chat or live updates on a dashboard. It might be confusing at first to know where to put the WebSocket state and events relative to React components and Redux state.

Redux Middleware is a good place to handle all your WebSocket needs, which I'll lay out here.

You can also see a working implementation of this method used in this minimal open-source chat application I made with React/Redux, TypeScript, and Socket.io. I opted to just use the browser's WebSocket API for this article, though.

Prerequisites Goals
  • Set up a Socket client and Redux middleware for handling a WebSocket connection
Using WebSockets

The WebSocket API is a Web API that makes it possible to open a connection between a client and server. If you wanted to make something like a chat application without a WebSocket, you would have to utilize polling, or continuously making API requests at an interval.

The client portion of the connection is set up with new WebSocket(url), and it handles open, close, and message events.

// Initialize WebSocket connection
const socket = new WebSocket('wss://my-websocket-url')
// Listen for open connection
socket.addEventListener('open', (event) => {
  console.log('You say hello...')
})

// Listen for messages
socket.addEventListener('message', (event) => {
  console.log('Incoming message: ', event.data)
})

// Listen for close connection
socket.addEventListener('close', (event) => {
  console.log('...and I say goodbye!')
})

// Send a message
socket.send('A message')

// Close websocket connection
socket.close()
Socket Class

I often like to make a little class API to make it slightly cleaner and easier to work with code, and not have to make any global variables. Though I'm not sure exactly why I feel that this is so much better than having const socket = new WebSocket(url) defined somewhere. But here's a class that connects, disconnects, sends messages, and handles events.

@/utils/Socket.js
class Socket {
  constructor() {
    this.socket = null
  }

  connect(url) {
    if (!this.socket) {
      this.socket = new WebSocket(url)
    }
  }

  disconnect() {
    if (this.socket) {
      this.socket.close()
      this.socket = null
    }
  }

  send(message) {
    if (this.socket) {
      this.socket.send(JSON.stringify(message))
    }
  }

  on(eventName, callback) {
    if (this.socket) {
      this.socket.addEventListener(eventName, callback)
    }
  }
}

export { Socket }

Now this can be used in basically the same way, and it makes it simple to work with multiple WebSockets, which I have needed to do on some projects. The example here is quite simple, but you might have more than just open, message and close events, and you might add other things into the class like checking for a heartbeat, applying retries, etc.

import { Socket } from '@/utils/Socket'

const socket = new Socket()socket.connect('wss://my-websocket-url')
socket.on('open', (event) => {
  console.log('You say hello...')
})

socket.on('message', (event) => {
  console.log('Incoming message: ', event.data)
})

socket.on('close', (event) => {
  console.log('...and I say goodbye!')
})

socket.send('A message')
socket.disconnect()
Creating Redux Middleware

Here is an example of Redux middleware:

const middleware = (params) => (next) => (action) => {
  const { dispatch, getState } = params

  if (action.type === 'action you want to intercept') {
    // Do something
  }

  return next(action)
}

You have access to the entire current state with getState, and you have access to dispatching an action with dispatch. Middleware is similar to a Redux thunk action.

So for the Socket middleware, you'll want to intercept the open and close events for the WebSocket that have been dispatched from elsewhere. It might be on login to the application, as with an entire chat application, or it might be on mount of a specific page, as with a live dashboard.

Using socket/connect and socket/disconnect actions, you can pass through an initialized instance of the Socket class created earlier. On socket/connect, you'll open the WebSocket connection and handle all events. On socket/disconnect, you'll close the connection.

@/middleware/socket.js
export const socketMiddleware = (socket) => (params) => (next) => (action) => {
  const { dispatch, getState } = params
  const { type } = action

  switch (type) {
    case 'socket/connect':
      socket.connect('wss://example.com')

      socket.on('open', () => {})
      socket.on('message', (data) => {})
      socket.on('close', () => {})
      break

    case 'socket/disconnect':
      socket.disconnect()
      break

    default:
      break
  }

  return next(action)
}

Now the connection should be initialized when you call socket/connect, and throughout the life of the application the WebSocket will be listening via the event handlers, until you end the connection.

Finally, you'll add the middleware to the store configuration, shown here using the Redux Toolkit method which is currently the officially preferred method.

@/store/index.js
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'

import { rootReducer } from '@/store/rootReducer'
import { Socket } from '@/utils/Socket'

export const store = configureStore({
  reducer: rootReducer,
  middleware: [socketMiddleware(new Socket()), ...getDefaultMiddleware()],})

Now wherever you decide to call the connect and disconnect functions within the React component will have the desired effect.

import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'

export const LiveDashboardPage = () => {
  const dispatch = useDispatch()

  useEffect(() => {
    dispatch({ type: 'socket/connect' })
    return () => {
      dispatch({ type: 'socket/disconnect' })    }
  }, [])
}
Conclusion

This is a very minimalist setup for getting a WebSocket connection into a Redux application, but it covers the main gist of it. Use Middleware to intercept connect and disconnect events, and listen to all the events upon initialization and handle them accordingly.

One thing to note with this particular approach is that every tab will open a new WebSocket connection. I've done some poking around but I haven't come to a definitive conclusion on whether or not 1 tab = 1 connection is the best approach, or whether using the SharedWorker API and BroadcastChannel API is a better approach. I plan to follow up this article with one about sharing a connection across multiple tabs using these APIs.

https://taniarascia.com/websockets-in-redux/
Understanding the GraphQL Type System
GraphQL is a modern solution for facilitating the communication between a front end and a data source. All of the details and capabilities…
Show full content

GraphQL is a modern solution for facilitating the communication between a front end and a data source. All of the details and capabilities of a GraphQL implementation are laid out in the GraphQL Schema. In order to write a functioning GraphQL schema, you must understand the GraphQL Type System.

In this article, you will learn about GraphQL types: the five built-in scalar types, Enums, the List and Non-Null wrapping types, Object types, and the abstract Interface and Union types that work alongside them. You will review examples for each type and learn how to use them to build a complete GraphQL schema.

Prerequisites

To get the most out of this tutorial, you should have:

Scalar Types

All the data in a GraphQL schema ultimately resolve to various scalar types, which represent primitive values. GraphQL responses can be represented as a tree, and the scalar types are the leaves at the ends of the tree. There can be many levels in a nested response, but the last level will always resolve to a scalar (or Enum) type. GraphQL comes with five built-in scalar types: Int, Float, String, Boolean, and ID.

Int

Int is a signed 32-bit non-fractional numerical value. It is a signed (positive or negative) integer that does not include decimals. The maximum value of a signed 32-bit integer is 2,147,483,647. This is one of the two built-in scalars used for numerical data.

Float

A Float is a signed double-precision fractional value. It is a signed (positive or negative) number that contains a decimal point, such as 1.2. This is the other built-in scalar used for numerical data.

String

A String is a UTF-8 character sequence. The String type is used for any textual data. This can also include data like very large numbers. Most custom scalars will be types of string data.

Boolean

A Boolean is a true or false value.

ID

An ID is a unique identifier. This value is always serialized as a string, even if the ID is numerical. An ID type might be commonly represented with a Universally Unique Identifier (UUID).

Custom Scalars

In addition to these built-in scalars, the scalar keyword can be used to define a custom scalar. You can use custom scalars to create types that have additional server-level validation, such as Date, Time, or Url. Here is an example defining a new Date type:

scalar Date

The server will know how to handle interactions with this new type using the GraphQLScalarType.

Enum Type

The Enum type, also known as an Enumerator type, describes a set of possible values.

Using the Fantasy Game API theme from other tutorials in the series, you might make an enum for the game characters' Job and Species with all the values the system will accept for them. An Enum is defined with the enum keyword, like so:

"The job class of the character."
enum Job {
  FIGHTER
  WIZARD
}

"The species or ancestry of the character."
enum Species {
  HUMAN
  ELF
  DWARF
}

In this way, it is guaranteed that the Job of a character is FIGHTER or WIZARD and can never accidentally be "purple" or some other random string, which could be possible if you used a String type instead of making a custom Enum. Enums are written in all-caps by convention.

Enums can also be used as the accepted values in arguments. For example, you might make a Hand enum to denote whether a weapon is single-handed (like a short sword) or double-handed (like a heavy axe), and use that to determine whether one or two can be equipped:

enum Hand {
  SINGLE
  DOUBLE
}

"A valiant weapon wielded by a fighter."
type Weapon {
  name: String!
  attack: Int
  range: Int
  hand: Hand
}

type Query {
  weapons(hand: Hand = SINGLE): [Weapon]
}

The Hand enum has been declared with SINGLE and DOUBLE as values, and the argument on the weapons field has a default value of SINGLE, meaning if no argument is passed then it will fall back to SINGLE.

Non-Null Type

You might notice that null or undefined, a common type that many languages consider a primitive, is missing from the list of built-in scalars. Null does exist in GraphQL and represents the lack of a value.

All types in GraphQL are nullable by default and therefore null is a valid response for any type. In order to make a value required, it must be converted to a GraphQL Non-Null type with a trailing exclamation point. Non-Null is defined as a type modifier, which are types used to modify the type it is referring to. As an example, String is an optional (or nullable) string, and String! is a required (or Non-Null) string.

List Type

A List type in GraphQL is another type modifier. Any type that is wrapped in square brackets ([]) becomes a List type, which is a collection that defines the type of each item in a list.

As an example, a type defined as [Int] will be a collection of Int types, and [String] will be a collection of String types. Non-Null and List can be used together to make a type both required and defined as a List, such as [String]!.

Object Type

If GraphQL scalar types describe the "leaves" at the end of the hierarchical GraphQL response, then Object types describe the intermediary "branches", and almost everything in a GraphQL schema is a type of Object.

Objects consist of a list of named fields (keys) and the value type that each field will resolve to. Objects are defined with the type keyword. At least one or more fields must be defined, and fields cannot begin with two underscores (__) to avoid conflict with the GraphQL introspection system.

In the GraphQL Fantasy Game API example, you could create a Fighter Object to represent a type of character in a game:

"A hero with direct combat ability and strength."
type Fighter {
  id: ID!
  name: String!
  level: Int
  active: Boolean!
}

In this example, the Fighter Object type has been declared, and it has has four named fields:

  • id yields a Non-Null ID type.
  • name yields a Non-Null String type.
  • level yields an Int type.
  • active yields a Non-Null Boolean type.

Above the declaration, you can also add a comment using double quotes, as in this example: "A hero with direct combat ability and strength.". This will appear as the description for the type.

In this example, each field resolves to a scalar type, but Object fields can also resolve to other Object types. For example, you could create a Weapon type, and the GraphQL schema can be set up where the weapon field on the Fighter will resolve to a Weapon Object:

"A valiant weapon wielded by a fighter."
type Weapon {
  name: String!
  attack: Int
  range: Int
}

"A hero with direct combat ability and strength."
type Fighter {
  id: ID!
  name: String!
  level: Int
  active: Boolean!
  weapon: Weapon
}

Objects can also be nested into the fields of other Objects.

Root Operation Types

There are three special Objects that serve as entrypoints into a GraphQL schema: Query, Mutation, and Subscription. These are known as Root Operation types and follow all the same rules as any other Object type.

The schema keyword represents the entrypoint into a GraphQL schema. Your root Query, Mutation, and Subcription types will be on the root schema Object:

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

The Query type is required on any GraphQL schema and represents a read request, similar to a REST API GET. The following is an example of a root Query Object that returns a List of Fighter types:

type Query {
  fighters: [Fighter]
}

Mutations represent a write request, which would be analogous to a POST, PUT, or DELETE in a REST API. In the following example, the Mutation has an addFighter field with a named argument (input):

type Mutation {
  addFighter(input: FighterInput): Fighter
}

Finally, a Subscription corresponds to an event stream, which would be used in conjunction with a Websocket in a web app. In the GraphQL Fantasy API, perhaps it could be used for random battle encounters, like so:

type Subscription {
  randomBattle(enemy: Enemy): BattleResult
}

Note that the schema entrypoint is often abstracted away in some GraphQL implementations.

Field Arguments

The fields of a GraphQL Object are essentially functions that return a value, and they can accept arguments like any function. Field arguments are defined by the name of the argument followed by the type. Arguments can be any non-Object type. In this example, the Fighter Object can be filtered by the id field (which resolves to a Non-Null ID type):

type Query {
  fighter(id: ID!): Fighter
}

This particular example is useful for fetching a single item from the data store, but arguments can also be used for filtering, pagination, and other more specific queries.

Interface Type

Like the Object type, the abstract Interface type consists of a list of named fields and their associated value types. Interfaces look like and follow all the same rules as Objects but are used to define a subset of an Object's implementation.

So far in your schema, you have a Fighter Object, but you might also want to make a Wizard, a Healer, and other Objects that will share many of the same fields but have a few differences. In this case, you can use an Interface to define the fields they all have in common, and create Objects that are implementations of the Interface.

In the following example, you could create a BaseCharacter Interface using the interface keyword with all the fields every type of character will possess:

"A hero on a quest."
interface BaseCharacter {
  id: ID!
  name: String!
  level: Int!
  species: Species
  job: Job
}

Every character type will have the fields id, name, level, species, and job.

Now, imagine you have a Fighter type and a Wizard type that have these shared fields, but Fighters use a Weapon and Wizards use Spells. You can use the implements keyword to delineate each as a BaseCharacter implementation, which means they must have all the fields from the created Interface:

"A hero with direct combat ability and strength."
type Fighter implements BaseCharacter {
  id: ID!
  name: String!
  level: Int!
  species: Species
  job: Job!
  weapon: Weapon
}

"A hero with a variety of magical powers."
type Wizard implements BaseCharacter {
  id: ID!
  name: String!
  level: Int!
  species: Species
  job: Job!
  spells: [Spell]
}

Fighter and Wizard are both valid implementations of the BaseCharacter Interface because they have the required subset of fields.

Union Type

Another abstract type that can be used with Objects is the Union type. Using the union keyword, you can define a type with a list of Objects that are all valid as responses.

Using the Interfaces created in the previous section, you can create a Character Union that defines a character as a Wizard OR a Fighter:

union Character = Wizard | Fighter

The equal character (=) sets the definition, and the pipe character (|) functions as the OR statement. Note that a Union must consist of Objects or Interfaces. Scalar types are not valid on a Union.

Now if you query for a list of characters, it could use the Character Union and return all Wizard and Fighter types.

Conclusion

In this tutorial, you learned about many of the types that define the GraphQL type system. The most fundamental types are the scalar types, which are the values that act as the leaves on the schema tree, and consist of Int, Float, String, Boolean, ID, and any custom scalar that a GraphQL implementation decides to create. Enums are lists of valid constant values that can be used when you need more control over a response than simply declaring it as a String, and are also leaves on the schema tree. List and Non-Null types are known as type modifiers, or wrapping types, and they can define other types as collections or required, respectively. Objects are the branches of the schema tree, and almost everything in a GraphQL schema is a type of Object, including the query, mutation, and subscription entrypoints. Interface and Union types are abstract types that can be helpful in defining Objects.

For further learning, you can practice creating and modifying a GraphQL schema by reading the How to Set Up a GraphQL API Server in Node.js tutorial to have a working GraphQL server environment.

https://taniarascia.com/graphql-type-system/
Year in Review: 2022 into 2023
Happy New Year, friends! Time to write another year in review. This will be the seventh time I've done this. Here are all the previous ones…
Show full content

Happy New Year, friends! Time to write another year in review. This will be the seventh time I've done this. Here are all the previous ones:

I have really high hopes for 2023! It's only a few days in and so far so good. I've really had a chance to relax, rest, and reset.

Here's me and Dimo at the end of 2022.

eoy2022tania

Here are a few bits of content out in the ether that I enjoyed this year.

  • The Bear (TV Show) - If you want to know what my life was like before changing careers into development, this show does the best job I've ever seen. It takes place in Chicago, to boot! I usually avoid food/kitchen related shows like the plague, but I'm glad I watched this one. I could very much relate to this show.
  • Reservation Dogs (TV Show) - I just watched the first season of this show last week, and I thought it was great. It's a slice of life show about four kids on a Native American/Indian reservation in Oklahoma.
  • 1883 (TV Show) - A really good Western themed show about a family on the Oregon trail. I was surprised how much I liked it. Highly recommended!
  • Avatar: The Way of Water (Movie) - I loved the first Avatar, and this one was great, too! It made me feel something, and I spent a few days thinking about it after watching. There's just something about night time bioluminescence that can't be beat.
  • Three-Body Problem Series (Book series) - A really good hard sci-fi series that I read this year. It was originally written in Chinese. It's very existential, and the scope of the story is bigger than most other sci-fi books I've ever read.
  • Obsidian (App) - I switched from Bear + Todoist to Obsidian for note-taking and todos. I've been looking for a long time for ONE app the data is all stored in Markdown on folders on my computer, can sync between Mac, Windows, and iPhone, and can handle todos as well as note-taking. I found it all with Obsidian! I pay the full $100/year or so for Premium (sync), I think it's worth it to keep everything together.
  • FanHOTS (Twitch) - The game Heroes of the Storm is my gaming addiction, and if long periods of time go by that I don't write, it's because I was probably playing HOTS instead. Every now and then, I decide I need to be productive instead and I delete the game. Fan is a great content creator with integrity who is highly entertaining as well. Despite the fact that HOTS is in maintenance mode and no longer has the player base of the heydey, he continues to play and teach, and I like to watch and support when I can.
Resolutions?

I gained a little more weight this year than I'd like, so I made a dashboard for tracking progress - weight, waist measurement, calories, BMI, alcohol intake, etc. Here's Weight Loss Dashboard (and Sandbox) if you think that's helpful and would like to do something similar.

I figure it's best to focus on one thing at a time, so I'm focusing on health/fitness/diet/exercise right now. My plan is to focus on one thing per month, so maybe one month will be all music, one month all drawing, one month all coding/learning, etc. We'll see!

It's easy to say January 1st is an arbitrary date and doesn't mean anything, but honestly perception can be powerful. For me personally, it was really hard to start any goal in December with all the parties and get togethers and routine disruption, and a nice, cold, boring January is a good time to start a new routine. It also gave me time to plan exactly what I want to do, which can also contribute to success.

I wrote 10 articles (11)

It wasn't an amazingly writing heavy year for me but! I wrote a few good ones. Five of them were written in December - I'm getting back into the swing of things! (It's actually 11, but one that I wrote for DigitalOcean has not been published yet.)

Technical Personal
  • Redesign: Version 6.0 - Yet another redesign this year. I'm really happy with the current design, so I think I'm going to keep it like this for a while!
  • Memories of Josh - Pictures and memories of my friend, Josh.
  • Tending to my Digital Garden - A personal post I made about the current state of life, burnout, and work.
  • An Ode to Animorphs - If you're into sci-fi at all, set aside your preconceived notions and give this article a read! I just want to get people into Animorphs lore in 2023.
I made 1264 commits

The commit metric is fun to track. The vast majority of my coding has been work-related. So as you can see, I've been busy M-F basically every week this year except for two small vacations! Still, on the personal side I managed to get a few demos/projects in this year, a redesign, and some posts.

2022 personal 2022 work

I made 1 project

My project this year was KeyboardAccordion.com, which I made to be able to practice the accordion at night and not bother anyone. The write-up for this project is the Musical Instrument/Web Audio API article.

I also started a Baba Is You game implemention earlier in the year that I abandoned for a bit, I'd like to finish that up this year.

I got a promotion

I've been making fewer updates here, but working as much or more than ever. I actually really enjoy my job and the people I work with, and greatly appreciate that I have a good work-life balance. I was recently promoted to Principal Software Engineer, after a few years working as a Staff Software Engineer. I've really come a long way in the past eight or nine years of working as a developer, and still have a long way to go!

I drew 10ish things

I bought a tablet this year! I made an Illustration page to keep track of what I draw. My plan to get back into the swing of things was to draw all 150 Pokémon. I haven't made it that far yet, but it's a start.

I recorded 1 song (and got an accordion)

I recorded one song this year, after a two year break.

I also got an awesome wooden chromatic button accordion I'm very excited about learning. I'm slowly teaching myself to play, but I'm completely self-taught when it comes to any kind of music and I can't really read sheet music, so it's slow going. I'm currently wondering if I can find a teacher in my area, but it's a pretty niche instrument so we'll see.

I learned a few songs on it already though - Korobeiniki (Tetris theme), Shostakovich Waltz No. 2, La Noyee, La Valse des Monstres, Sonic Accordion Song, and Final Fantasy VI Wedding Waltz.

I did a lot of interior decorating

My entire life up until now, I never really cared much about my environment: I lived in small, cheap apartments with cheap furniture and little to no art. I'm not sure why exactly - I was frugal, but also it felt like the apartments were just supposed to be temporary, so why would I put time and effort into decorating them?

I've stepped away from the minimalism mindset lately. I spent a lot of time this year buying art and furniture for my latest apartment, and making a cool battlestation for WFH + play.

Now I have art on the walls from the Chicago transit system, Cowboy Bebop, Animorphs, Harry Potter, russian propaganda, space, my own paintings, and small designs from artists around the city. For once I really enjoy my environment and I'm glad I put the time and effort into it.

What I want to write about

A few topics I have ideas about and would like to write about in the coming year:

  • WebSocket API, BroadcastChannel API, SharedWorker API
  • Swagger/OpenAPI
  • Common array/object tasks
  • Redux + Immer
  • Drawing in Photoshop
  • Logical fallacies
  • Existentialism
  • Internet in the '90s
What I want to learn next

A few topics I'd like to learn more about in the coming year:

  • Redis
  • Pub/Sub
  • Why everyone loves Tailwind
  • Server-side rendering (NextJS?)
  • Python environment setup
  • CSS animations (This is on my list of things to learn from 2016...)
  • Data structures (On my list from 2019...)

Sometimes it's hard for me to realize how much I've learned over the last several years, but the list is actually getting quite long. It's not quite so obvious what I should learn next, suggestions are appreciated!

Maybe Rome, Deno, WebAssembly, web components, newer CSS concepts, in-depth exploration of DevTools, or Rust?

Conclusion

This year I started writing a bit more about personal stuff - my friend, my childhood Animorphs obsession, burnout. It's been good practice for me to write in a format that isn't just an A-to-B tutorial. It's actually quite a struggle for me to focus and do reseach, which is why I haven't written so much of those types of posts, but we'll see what interests me in 2023.

I think over the last few years, I've become much less of a "known entity" in the JavaScript/front end world. I write less, the topics I do write about tend to be more focused and advanced, and I don't participate much in online discussion. Unfortunately, that means my connections with other people dwindle, so I hope in 2023 I can make some more good, genuine, human connections through this website, and in the real world as well.

Thank you all for reading, and I hope you have a great 2023!

https://taniarascia.com/2022-into-2023/
The Lore of Animorphs (an Ode)
Recently, after I watched the latest movie in the Avatar series - yes, the one with the blue people - it got me thinking about why I like…
Show full content

Recently, after I watched the latest movie in the Avatar series - yes, the one with the blue people - it got me thinking about why I like the series so much, and I realized it's not the only "blue alien falls in love with human ultimately resulting in inter-species transmogrification" story that I love. That got me thinking about Animorphs.

Actually, I never stopped thinking about Animorphs. Despite the fact that I read through the series when I was in grade school, and despite the fact that it was written for children, the dark themes and situations, cast of complex characters, and moral quandaries presented within have stuck with me for my entire life.

I know what you're thinking - Animorphs, really? Those weird kids' books with the embarrassing 90s cover art? The low-budget, forgettable Nickelodeon show? The next generation of the Babysitter's Club? The rag-tag Captain Planet squad of kids? Maybe you read a few of them as a kid and it didn't really stick with you, or maybe you were too old or too young to bother. It's hard to imagine that anything but nostalgia that could make me make such bold claims about Animorphs being so deep.

animorphs

Animorphs was dark. Animorphs was often disturbing, and no one got a happy ending. War, morality, ethics, genocide, torture, psychopathy, love, betrayal, loss of innocence, and sacrifice are all themes that were explored in Animorphs.

I don't think anyone who didn't engage with the series understands or cares to, and why should they? But oddly I feel like this important part of my childhood and myself is misunderstood as a result and I have a desire to get more people to understand what I felt. Although I haven't read any of the books since I was a teenager, and the writing is very simple so my childhood imagination read between the lines and filled in the blanks, many quotes and situations throughout the story still evoke emotion and still make me think about them, and I want to share them with you.

So let's start at the beginning...

(Massive spoilers ahead)

Seerow's Kindness

The story does not begin with Earth, humans, or the Animorphs. Two alien species, the Yeerks and the Andalites, are engaged in a bitter war spanning the galaxy and humans eventually become unknowing pawns in this war. But before that there was Seerow's Kindness.

Prince Seerow was an Andalite scientist and researcher that led the first expedition to the Yeerk homeworld. Andalites are a highly intelligent and advanced race of space-faring aliens that look like blue centaurs with no mouths that communicate with their minds using thought-speak. They also recently developed a technology that allows them to absorb the DNA of creatures they touch and morph into them at will.

When Seerow arrives on this new planet, he discovers two sentient species that evolved and live in an almost symbiotic harmony: a highly-intelligent and slug-like parasitic species known as the Yeerks, and a host species with low intelligence and poor motor skills known as the Gedd. Yeerks can burrow into the ear canal of a sentient species, wrap around the brain stem, and take control of the host. In their natural state, Yeerks are blind and helpless, and need to return to their pool to feed every three days.

Seerow befriends the Yeerks and teaches them writing, science, technology, and space travel. He takes pity on them and believes they have the right to join other sentient races, and seeks to form a peaceful coalition of cooperation and understanding between the two species. This era of harmony doesn't last long - a group of Yeerks rebel and betray the Andalites, steal several ships and escape to space with intent to expand their empire, find new hosts, and not be forever playing second fiddle to the Andalites.

You're a fool, Seerow. A soft, sentimental, well-meaning fool. And now my men are dead and the Yeerks are loose in the galaxy. How many will die before we can bring this contagion under control? How many will die for Seerow's kindness? — Alloran

This event leads to the start of the Andalite-Yeerk War, and the creation of the sarcastically named Seerow's Kindness law, in which Andalites must never give their advanced technology to any other race.

Already, I'm intrigued by how the supposed heroes and villains are compelling and not so black-and-white. The war does not begin with a conquest for land or a fight for ideology, but with an innocent act of naive kindness. Although the Andalites are our supposed heroes, they're full of arrogance and pride. And although the Yeerks are our clear villains, it is possible to empathize with their situation.

The First Innocents

After the events on the Yeerk homeworld, the disgraced Seerow is exiled to a new planet with his family. This new planet is the homeworld of the Hork-Bajir, a fearsome looking but peaceful race of tree-dwelling reptilians. The Hork-Bajir are a simple tribal people that have not even discovered music or art yet. Once every several generations, a special Hork-Bajir capable of deep understanding and intelligence known as a seer is born to the people, who is seen as a harbinger of change.

One such seer, named Dak Hamee, is living on the Hork-Bajir homeworld when Seerow and his family are stationed there. Seerow's daughter Aldrea befriends Dak, and they begin to form a bond sharing each other's culture and knowledge.

aldrea

Soon enough, the Yeerks show up for their first planetary conquest, and Aldrea's family are among the first casualties. The Hork-Bajir have no concept of killing or war, making them incredibly simple to subjugate and turn into Controllers. Aldrea encourages Dak to teach his people to fight, and together they form a resistance force against the Yeerks.

"You are the seer. You were born to teach your people a new thing. Maybe you were born to teach your people to fight."
"I hoped I had been chosen to show my people all the things your father tried to show the Yeerks. I wanted to teach them music. Writing. Art..." — Aldrea and Dak

Dak leads his people knowing it's the only way to save them, but feels a deep remorse for introducing war and violence to his people who only knew peace.

"We don’t unleash a plague of parasites on the galaxy, endangering every other free species, and then go swaggering around like the lords of the universe. No, we’re too simple for all that. We're too stupid to lie and manipulate. We're too stupid to be ruthless. We're too stupid to know how to build powerful weapons designed to annihilate our enemies. Until you came, Andalite, we were too stupid to know how to kill." — Dak

Aldrea believes the Andalites will show up and save them, but the Andalite force led by War-Prince Alloran that eventually does arrive is not enough to turn the tide of the war. After many months of guerilla warfare, Alloran turns to drastic measures and plans to release a biological weapon called the Quantum Virus on the planet. The virus targets Hork-Bajir biology, and would kill all the Hork-Bajir on the planet, whether controlled by Yeerk or not.

"You ask me to kill my own people today and to lead my people in killing their brothers. You say they are not Hork-Bajir, but Yeerks. But when the dead have given up their souls to Mother Sky, there will be Hork-Bajir bodies lying dead." — Dak

When Aldrea learns of this plan, she and Dak try to stop Alloran but are thwarted by a Yeerk named Esplin 9466, ranked Sub-Visser Twelve in the current Yeerk regime and one of the first Hork-Bajir-Controllers. They fail to prevent the release of the virus. Aldrea becomes disillusioned with her species, defects and morphs into a female Hork-Bajir. A morph becomes permanent if two hours pass without returning to the original form, and Aldrea becomes a Hork-Bajir forever.

The war is lost; Alloran and his Andalite forces abandon the planet and Alloran, now disgraced, is henceforth known as the Butcher of Hork-Bajir. Aldrea and Dak hide away in a deep valley the virus does not reach, and have a child they name Seerow. Eventually the Yeerks take full control of the planet and its inhabitants, and completely ravage the natural habitat. Aldrea and Dak are killed, and their child is made into a Controller.

Would he leave me? No. He cared for me. We had more in common than he could ever have with any Hork-Bajir. It was too late for Dak: He knew that the stars were not flowers. — Aldrea

This story was a lot to take in. It really shows that everyone loses in war. Dak's loss of innocence, Alloran's loss of humanity, Aldrea's loss of hope and optimism, the innocents caught in the crossfire. It becomes increasingly difficult to root for the Andalites when they're advocating for actual genocide. I appreciate that K.A. Applegate respected her readers enough, young children that we were, to be able to engage with these themes.

Although Aldrea, Dak, their child, and the Hork-Bajir homeworld are doomed, all hope is not completely lost. Many generations down the line, a seer will be born of their descendants who, having grown up in the midst of war, has no reservations about fighting back.

The Discovery of Humans

The end of the Hork-Bajir War is far from the last we see of disgraced War-Prince Alloran. Many years later, well into the Andalite-Yeerk War, Alloran - no longer in a position of much importance, although the atrocities he committed have been mostly kept quiet - oversees a simple transport mission with two Andalite cadets, Elfangor and Arbron.

elfangor

Their task is to return two humans back to Earth: Loren and Chapman, who have been essentially abducted by a mischiveous species of aliens called the Skrit Na. True to character, Alloran has no qualms ordering Elfangor to flush the cargo on the ship - thousands of defenseless Yeerks. Elfangor refuses. Still, it seems Alloran's time in the last war has left him with some mental scars as well.

"So, Loren, Daddy went nutso, huh? Another whacked-out 'Nam vet? I guess some guys can't take it."
"Be quiet, fool. Those who have been to war understand. Those who have not have no opinion worth hearing. Even those who return from war may never really come home." — Chapman and Alloran

Elfangor and Loren discover that they are curious about each other's life and culture and begin to form a friendship on the journey.

"Do humans dream?"
"I do. Every night."
"So do I." — Elfangor and Loren

Soon the mission is interrupted, as the Andalites discover that the Skrit Na also inadvertently made off with a machine known the Time Matrix, the most potentially dangerous weapon in the galaxy. The Time Matrix is known in Andalite lore as having been created by an all-powerful being called the Ellimist, but Elfangor thought both the Time Matrix and the Ellimist were just a myth. The Andalites are desperate to stop the Skrit Na before they realize what they possess and sell it to the Yeerks. The humans get strung along as the Andalites head to the Yeerk colony on the Taxxon homeworld.

The Taxxons are a sentient, hive-minded species that resemble enormous centipedes and live in a state of eternal hunger - a raging, insatiable and irresistible instinct to consume. Unlike other species, the Taxxons willingly exchanged their freedom for the promise of a constant supply of fresh meat from the Yeerks. Even Yeerk Controllers cannot overpower a Taxxon in a bloodlust-induced feeding frenzy. Taxxons will not even hesitate to turn to cannibalism when faced with a wounded of their own kind.

The Yeerk Esplin is stationed on the Taxxon homeworld, his rank now upgraded to Sub-Visser Seven. Having realized that a Yeerk's host body becomes a political indication of power, he is obsessed with idea of being the first to infest an Andalite.

"There is one other possibility, Andalite. There has never been an Andalite-Controller. None of us has ever succeeded in capturing an Andalite alive. Your warriors use that nasty Andalite tail blade on themselves rather than be taken alive. Such a waste. Really. See, I want to be the first to have an Andalite body." — Esplin 9466

The three Andalites acquire Taxxon morphs on the homeworld, and do some reconnaissance before getting separated during a frenzy. By the time Elfangor and Arbron meet up again, Elfangor realizes with horror that Arbron is still in his Taxxon form - Arbron had passed the two-hour time limit and will forever be trapped in a Taxxon body.

Initially, Arbron tries and fails to get Elfangor to kill him. Elfangor refuses and they get separated (and then Elfangor has a whole montage of discovering Earth through cigarette ads in a magazine and driving a Mustang through the Taxxon desert while drinking Dr. Pepper and blasting The Rolling Stones...) by the next time Elfangor and Arbron meet, Arbron had discovered the Living Hive and the Taxxons who were still loyal to their own kind, and decides to aid their war of resistance against the Yeerks.

"Don't pity me, Elfangor. I am glad I didn't die. Any life is better than none. And no matter how awful things seem, there is always meaning and purpose to be found." — Arbron

Elfangor meets back up with Alloran who has captured the Yeerk Esplin, who he plans to use as leverage to get off the planet. Once again Alloran commands Elfangor to flush the Yeerks, but once again Elfangor refuses and discovers the reason for Alloran's disgrace.

"They are the enemy. Hypocrites! You're all hypocrites! We lost the Hork-Bajir war because of weak, moralizing fools like you! Because of fools like you, I am disgraced and shunned and sent off on trivial errands. What is the difference how you destroy the enemy? What does it matter if you kill them with a tail blade or shredder or quantum virus?" — Alloran

Secretly working together, Chapman knocks out Alloran and Esplin infects Alloran's body and becomes the first Yeerk controlled Andalite.

He stood there, rage on his face. Alloran. War-Prince Alloran-Semitur-Corrass. But not really Alloran anymore. For the rest of my life I would remember that moment. The moment when I looked for the first time upon the abomination. — Elfangor

Elfangor and Loren manage to escape with the Time Matrix on the ship, but they can't evade the Sub-Visser for long, who has now been elevated to a Visser position. Eventually, all three fight for control of the Time Matrix, and get transported to a new world. When Elfangor comes to, he sees the Guide Tree from his homeworld and believes himself to be home, but looking up at the sky he notices the red and gold of his own sky interspersed with the blue sky and clouds of Earth and lightning-torn green sky of the Yeerk homeworld. The memories and desires of Elfangor, Loren, and Esplin combined created a patchwork world.

Elfangor finds Loren and as they travel through this newly-devised world, it only becomes more bizarre. Any sentient beings they encounter turn out to be simple representations formed from bits and pieces of their memories. Loren sees a boy she knows, but his splotchy, pustule ridden face is completely devoid of eyes, as his acne was all Loren ever noticed of him in the real world.

Eventually they find the Time Matrix in a spiral at the center of the new universe. Elfangor decides due to the shame of being responsible for the existence of the first Andalite-Controller, the horror leaving Arbron behind, and the knowledge of what Andalites have done and what they might do with the Time Matrix technology, that he will go with Loren back to Earth.

Elfangor decides to leave the war behind forever and morphs into a human, taking the name Alan Fangor. He buries the Time Matrix deep in a forest. He goes to college to be a computer scientist (and teaches his friends Bill and Steve a few things). And he marries Loren. Elfangor starts a new life, and for three years he lives as a human far away from the battles raging across the galaxy on distant stars.

However, Elfangor's self-imposed exile will not last. The Ellimist, creator of the Time Matrix, arrives to right the wrongs of the alternate timeline Elfangor has created. He gives Elfangor the choice to stay and allow the galaxy to fall into chaos under the Yeerks, or return and become the War-Prince he was meant to be.

"You refused to slaughter defenseless prisoners. You refused to destroy yourself in order to win a battle. You are wise, for a primitive creature. But you also altered the course of time by using the Time Matrix. And that has created awful problems. For your people. For both your peoples. Your peoples need you. You are not where and when you should be, Elfangor." — Ellimist

The Ellimist conveniently fails to mention that Loren was pregnant before restoring the timeline, erasing her memories of all that had conspired.

"No! You can't take me away! I have a son! That changes everything! Don't take me away!" — Elfangor

Although Elfangor leads his people to victory in many battles, his already tragic story will not end any happier. After crash-landing in an abandoned construction site on Earth during a battle with Esplin - now upgraded to Visser Three - he encounters five young humans. In a last-ditch effort, he breaks the law of Seerow's Kindness and gives them the morphing technology before being mercilessly devoured by Visser Three in morph.

In a small thread of hope, one of the humans with the new technology is his son, Tobias. Tobias's story is hardly any less tragic - abandoned by his mother at birth, he grows up being shuffled between an alcoholic uncle and apathetic aunt. Tobias experienced no love in his childhood. When Tobias gets trapped in the body of a hawk and disappears from human life, there is no one to notice or care.

The Ellimist intervenes in its twisted way, giving Tobias the ability to become a human, but only for two hours at a time, leaving the hawk form as his permanent body. Even as Tobias begins to find meaning in his life through his friends and the battle against the Yeerks, he ultimately gets kidnapped and tortured, and sees the woman he loves get murdered before his eyes. Tobias never recovers, severing his last remaining connections with humanity. Maybe the Ellimist should have just let Elfangor be a dad.

Our story leaves off in the middle of the war, but does not explain how the Yeerks came to infiltrate humanity...

The Sharing

The Yeerks, now let loose upon the galaxy, are in search of what they consider a Class Five species: a species that is a viable for infestation with no biological drawbacks, that exist in large numbers and do not possess the military power and technology to present a challenge for invasion.

A low-ranking Yeerk named Edriss 562, ranked Sub-Visser 409, hears about humanity through Esplin's report from the Taxxon homeworld with Loren and Chapman. Suspecting that humans might be a Class Five species and desiring the glory of discovering them herself, she hijacks and ship and heads for the region where Earth is thought to be, along with a subordinate named Essam.

After obtaining information from their first victim, a solider in Desert Storm, the Yeerks head to America, believing it to be the most powerful sector - and Hollywood to be the most important, according to the broadcasts they discovered. Edriss finds and infests a drug addict known as Jenny Lines, and Essam's finds a host in a movie producer she is acquainted with.

"This human has suffered what the humans consider to be the most horrific torture and deprivation in their history. An experience in his youth that even a Taxxon would find cruel. I believe he has weaknesses, but is not weak."
"No, Essam, you are wrong. They are not a strong species with a few weaknesses. They are weak, with but a few strengths. We will not have to conquer humans. They will conquer themselves. They will come to us willingly and make themselves our slaves." — Edriss and Essam

In need of a more useful host, Edriss disposes of Jenny Lines and infects a scientist named Allison Kim, and she begins to learn more about the resiliency of the human spirit.

Allison fought me. What a glorious fight she made of it! I used to toy with her, withdraw some small bit of my control, just to see how long it would take her to find the weakness and attempt to exploit it.
Once I surrendered control of a single eye. Allison discovered that she could change the direction of that one eye. She waited a week, til I was driving a car on a busy road, going at a high speed. Then, at the perfect moment, she closed her eye. She had been trying to kill herself, and me. Better dead than a Controller. — Edriss

Essam, in turn, takes a new host named Hildy Gervais. Far from the Empire and sentenced to death for disobeying orders, Essam and Edriss begin to fade into human life, reveling in their new senses. Through Allison's romantic attraction to Hildy and Edriss's feelings for Essam, the four fall in love, resulting in Edriss becoming the mother of two human children through her host body.

"I love you, Edriss. And I love these small humans. Our children. One thing we swear, the four of us, the children will survive." — Essam

Caught between her ambitions and her desire to bask in the experience of being a human, Edriss decides she must find a way to infiltrate humans from within.

Starvation lay ahead. Essam said he would die rather than contact the Empire. Not me. I wasn’t ready to die. I loved life as a human. Loved my life as Allison Kim, as Hildy's wife, as a mother. — Edriss

Edriss invents the idea of The Sharing, a front organization and subtle cult for vulnerable people (similar to the real life Landmark). She takes a secondary host to be the leader, and uses San Francisco as a base.

It would cater to one of the most fundamental human weaknesses: the need to belong. The fear of loneliness. The hunger to be special. The craving for an exaggerated importance. I would make a haven for the weak, the inadequate, the fearful. — Edriss

The Sharing consists of an Outer-Sharing and Inner-Sharing. To any observer, the Outer-Sharing seems like a community center with events like cookouts and rafting trips; something to bring your friends and family to. If and when a person voluntarily decides they wish to join the Inner-Sharing, they are told they will become a part of something beautiful and bigger than themselves, discover their true potential and be the best person they can become.

Joining the Inner-Sharing means willingly submitting and being infested by a Yeerk. In this way, the slow subjugation of Earth begins, unbeknownst to the general human population. After the first human submits, Edriss contacts the Empire with proof, and the plan is put into operation.

Horrified by everything that's happening, Hildy/Essam rebel against Edriss, free Allison, and escape with the children, but it doesn't last long. Edriss manages to track them down, kills Allison, and tears Essam in half as he crawls out of Hildy's ear, killing Essam and rendering Hildy insane.

Edriss rises to the rank of Visser One, the highest military rank among the Yeerks, answering only to the Council of Thirteen. She takes a new human host, a woman named Eva, and stages her death. However, Eva has a son named Marco, who happened to be one of the kids walking through the abandoned construction site on that fateful night...

And Finally, the Animorphs

Everything I've mentioned so far has been backstory and build-up to the Animorphs.

Animorphs was a book series that came out in 1996, when I was seven. Even as a young grade schooler I was embarrassed by the cover, but I was enthralled by the story. Every month, a new book would come out, and if I could scrounge up five dollars, I'd buy it.

When I think back on it, the books had even more of an impact on my young self than I remembered - my first ever online screenname was Morph, one of the first websites I ever made was a little Animorphs fan site, and something about Andalite thought-speak and HTML is <forever linked> in my mind.

The books were not all of particularly high quality - in order to meet the monthly quotas, ghost writers were hired, and the books in the middle of the run were often questionable filler - but the first books, last books, and supplemental world building books were highly engaging.

ellimist

The Animorphs follows our Breakfast Club cast of misfit who were in the abandoned construction site that night: Jake, the fearless leader; his cousin Rachel, a popular girl at school who finds more joy in the fighting than expected; "kill 'em then cry over them" Cassie, the team's voice of morality; Marco, the comic relief and the son of Visser One's host; and Tobias, the Three Wolf Moon shirt-wearing dweeb with a sad upbringing. And later Aximili, Elfangor's younger brother.

They can't tell you their full names. They can't tell you where they live. Because nothing is safe, and anyone you know - your brother, your dad, your teacher, your best friend - might be under a Yeerk's control right now.

Before Elfangor died, he gave the group the power to morph and told them the Andalite fleet might take a year or more to show up. If they don't want Earth to be completely taken over by then, they'll have to fight. At first the kids shrug it off, but Jake soon discovers that his brother, Tom, is a Controller. The group goes on their first mission to save Tom. They fail - and Tobias gets forever stuck in his hawk body - but from then on they become the Animorphs.

Right from the start, the stakes are high. Tobias's human life is over, and Jake's brother is one of them. As the series progresses, it only gets darker as they have to deal with the horrors they're committing and their own mental torment.

Rachel, once just a pretty girl who liked shopping and gymnastics, discovers that she revels in the thrill of violence and killing in her new Animorphs role. She takes the form of a grizzly bear, and is always down for any plan, no matter how dangerous or gruesome. She becomes the one who does the dirty work, the deeds no one else in the group wants on their conscience.

Lately, it's been scaring me that I like it. That I look forward to it so much. I can't help myself. It's like I'm addicted or something. Addicted to danger. Addicted to defeating the Yeerk invaders. - Rachel

At one point, another human named David gains the ability to morph but turns out to be a sadistic psychopath. Rachel is the one who carries out a plan to trap him in a cage until he's stuck in rat morph, and leave him stranded on an island as he pleads for Rachel to kill him.

By the end of the series, Jake has to give the order to Rachel to kill his brother, Tom, who is the host for a high-ranking Yeerk. He also gives the order to flush 17,000 Yeerks in a pool into space.

Tom was dead. And I wondered how I was ever going to explain it. I had ordered my cousin to execute my brother. How would I ever explain that? All these years I'd fought to keep us all alive, to stop the Yeerks, always with the hope that someday I would save my brother, that he would come back, that he'd be Tom again. That was why I'd enlisted in the war to begin with. I was going to save Tom. Tom was dead. The Yeerk in his head was dead. And Rachel. - Jake

Rachel also dies and Tobias, who loves her, loses the last shred of what keeps him tethered to humanity, escaping to the woods and living fully as a hawk.

And ultimately, the whole story is actually a universe-spanning 4D chess game between two powerful entities, the Ellimist and the Crayak (similar to the Vorlons and the Shadows in Babylon 5) with all the rest of the characters just being pawns in their war.

The Author's Take

Nobody got a happy ending in Animorphs, which did not sit well with many readers. K.A. Applegate wrote a letter to the fans with her thoughts about it.

Animorphs was always a war story. Wars don’t end happily. Not ever. Often relationships that were central during war, dissolve during peace. Some people who were brave and fearless in war are unable to handle peace, feel disconnected and confused. Other times people in war make the move to peace very easily. Always people die in wars. And always people are left shattered by the loss of loved ones.

So, you don’t like the way our little fictional war came out? You don’t like Rachel dead and Tobias shattered and Jake guilt-ridden? You don’t like that one war simply led to another? Fine. Pretty soon you’ll all be of voting age, and of draft age. So when someone proposes a war, remember that even the most necessary wars, even the rare wars where the lines of good and evil are clear and clean, end with a lot of people dead, a lot of people crippled, and a lot of orphans, widows and grieving parents.

If you’re mad at me because that’s what you have to take away from Animorphs, too bad. I couldn’t have written it any other way and remained true to the respect I have always felt for Animorphs readers. — K. A. Applegate
Recap

As I mentioned, a slew of dark and disturbing scenarios play out in Animorphs.

  • Ethics and morality - The entire existence of Yeerks, and the fact that the only way they get to live a life that isn't completely devoid of all senses is through enslaving another creature, how far the Andalites and Animorphs will go while still being "the good guys", the killing of innocent hosts in battle
  • Genocide - Alloran's usage of biological weapons against the Hork-Bajir, multiple occasions in which the Animoprhs eject thousands of defenseless Yeerks into space, or boil them alive in their own pools, the Ellimist's home planet being annihilated by another species when they think his simulation games are reality
  • Torture and body horror - Tobias being relentlessly tortured by Taylor, a voluntary Controller who chose to betray humanity after being disfigured in a house fire, the Ellimist being forced to engage in mental games with a large, borg-like alien for centuries as his body hangs from a tentacle, surrounded the rotting corpses of his friends, countless situations in which the Animorphs almost get stuck trapped in horrible morphs like fleas and ants, Animorphs losing limbs and almost bleeding out during battles, Elfangor almost being eaten alive as a Taxxon
  • Suicide - Allison Kim attempts to commit suicide rather than be a Controller
  • PTSD - Jake's inability to cope after the war, Tobias's escape from humanity
  • Love - Several stories of alien/human inter species love, such as Aldrea and Dak, Elfangor and Loren, and Visser One's strange four-way relationship. Of course, none of these ended well, and even within the Animorphs, Tobias and Rachel's story ends tragically, and Jake and Cassie grow apart after the war and part ways, as Cassie can never forgive Jake for the atrocities he's committed against his own brother, Rachel, and the Yeerks
  • Psychopathy - Rachel's slow descent into an obsession with the war
  • Not black and white - So much of the fiction I read as a child had pretty clear divisions without much deviation: Gryffindor good, Slytherin bad. Redwall mice good, rats bad. In Animorphs, there are Yeerks that fight for peace, and Andalites and humans that commit all manner of war crimes.
  • Just a host of other messed up stuff - Rachel trapping David as a rat, a Yeerk discovering they can stay alive via cannibalism, Arbron being stuck forever in a Taxxon morph, Taxxons hunger being so overwhelming that they will auto-cannibalize if wounded, Jake recruiting physically disabled kids to be Animorphs by the end of the war and then sending them to die on suicide missions, a peaceful android species turning to violence and being forced to forever remember every detail of what they've done, Marco having to struggle with his resolve to kill his own mother if it comes to it, etc.

And this is a broad take of the things I remember. So much of this has stayed with me, despite the fact that I read the whole series before I was even a preteen.

Conclusion

I really don't know if I'd recommend an adult to read Animorphs. Most of these books I read when I was 7-10 years old, and while the themes are very adult, the prose is very simple and full of onomatopoeia, and the books are full of dated '90s references, and there are a lot of them. As an adult, it's much harder to read between the lines and use your imagination to fill in all the blanks.

I don't tend to have a very good memory for the details of most books after reading them, but Animorphs has stayed with me through the years. I'm sad that the TV show was so bad, and I have little hope for any good adaptation, but I find the world created around Animorphs to be fascinating, as well as the feelings that come along with all these stories. I wrote this article to share the backstory of this world with an audience who I don't expect to ever read the books.

And there will always be a soft spot in my soul for a blue alien who falls in love with a human.

https://taniarascia.com/animorphs/
Testing API Calls With React Testing Library and Jest
Ever since Hooks were released in React, we've been using the hook to fetch data, whether directly or abstracted away behind Redux Thunks…
Show full content

Ever since Hooks were released in React, we've been using the useEffect hook to fetch data, whether directly or abstracted away behind Redux Thunks. Figuring out how to test those scenarios can be really frustrating at first, but fortunately it ends up not being very complicated. I'll show you how to test useEffect with both successful and failed API calls.

Prerequisites Goals
  • Set up a very simple React app with testing using Jest and React Testing Library
  • Write a test for when the API call succeeds
  • Write a test for when the API call fails
Setting up the Application and Test Environment

Feel free to skip this part if you want to get right to the good stuff. As I wrote this article, I decided to start with absolutely nothing to see what the bare minimum I could get away with. I wanted all the config files, setup, and modules to get a React environment up and running that outputs a running application and runs tests, with the most up-to-date versions of everything.

I know Vite and Rome and Rollup and lord knows what else are all the rage right now - I'm just using a simple webpack setup because it still works and I care more about just showing the tests in this article. However, please leave a comment to enlighten me on some of the improvements they bring to the table!

So here's the quick application setup.

File structure

What I ended up with looked like this:

.
├── dist
├── node_modules
├── src
│   ├── App.js
│   └── index.js
├── tests
│   └── App.test.js
├── .babelrc
├── jest.config.js
├── package.json
├── setupJest.js
└── webpack.config.js

I won't force you to go on the whole journey as I did, figuring out what was needed, but I'll cut right to the end and let you know all the packages.

Required packages

For the application, React + React DOM was necessary, as well as a few Babel packages.

npm i \
react \
react-dom \
@babel/preset-env \
@babel/preset-react

For setup, bundling, and compilation, webpack, webpack CLI, and a Babel loader were necessary.

npm i \
webpack \
webpack-cli \
babel-loader

And for testing, Jest, JSDom, and React Testing Library were necessary. I also brought in a Jest Mock package because it makes life easier.

npm i -D \
@testing-library/jest-dom \
@testing-library/react \
jest \
jest-environment-jsdom \
jest-fetch-mock

So these are all the packages necessary to get an environment up and running that spits out an application and tests it. Of course, there are some additional quality of life improvements you'd want, like the webpack serve dev server for hot reloads, but it's not necessary.

Config files Babel

Of course, there's the Babel config file, the same one you've probably been using for years.

.babelrc
{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}
webpack

And the webpack config file. It makes most of the decisions by default, such as using index.js as an entry point and outputting to a dist folder. I just needed to add a module to tell it to use babel-loader.

webpack.config.js
module.exports = {
  mode: 'production',
  module: {
    rules: [{ test: /\.js$/, use: ['babel-loader'] }],
  },
}
Jest

As for the Jest config file, I just needed it to use jsdom and set the right directories.

jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  rootDir: '.',
  modulePaths: ['<rootDir>'],
  moduleDirectories: ['node_modules', 'src'],
  setupFilesAfterEnv: ['<rootDir>/setupJest.js'],
}

Finally, in setupJest.js, we just want to enable jest-fetch-mock and import the Jest DOM.

setupJest.js
require('jest-fetch-mock').enableMocks()

import '@testing-library/jest-dom'
Package

Adding a script to package.json that just runs webpack allows you to test the build and ensure the application is running. I also added a jest command for the test. Everything else is just the packages brought in by the commands.

package.json
{
  "scripts": {
    "test": "jest --coverage",
    "build": "webpack"
  }
}

So that's everything as far as config for both the application and testing, now to set up the simple app.

App files

Not too much has changed as far as the React index file goes. The ReactDOM import and API is slightly different from the last time I used it, and StrictMode seems to be the default mode, so I'm just rendering to the #root and pulling in a component.

index.js
import React from 'react'
import ReactDOM from 'react-dom/client'

import { App } from './App'

const root = ReactDOM.createRoot(document.getElementById('root'))

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

So now comes the code we're going to be testing, App.js. I'm just going to make this file do some "componentDidMount"-esque fetching of data. I know it's not realistic for this to be done in App.js, but the way the code will be written will be pretty similar in a production level app, it'll just be somewhere else further down.

I'm going to use JSON Placeholder for the example API, but testing will be the same with your own internal APIs, a wrapper around fetch, and even if you're using Redux or some other state management.

So I'll start off with a title and a message, and start setting up the state we'll use: users for the data coming in, and error in case an error gets thrown. I could have added in some loading state, but I just kept it really simple. It should be easy to figure out testing the loading states after reading this article.

App.js
import React, { useState, useEffect } from 'react'

export const App = () => {
  const [users, setUsers] = useState(null)
  const [error, setError] = useState(null)

  return (
    <>
      <h1>List of Users</h1>

      <p>No users found</p>
    </>
  )
}

Now I'll add in the useEffect that fetches data from the API and updates the data state, otherwise updates the error state.

App.js
import React, { useState, useEffect } from 'react'

export const App = () => {
  const [users, setUsers] = useState(null)
  const [error, setError] = useState(null)

  useEffect(() => {    const fetchAllUsers = async () => {      try {        const response = await fetch('https://jsonplaceholder.typicode.com/users')        const data = await response.json()        setUsers(data)      } catch (err) {        setError('Something went wrong!')      }    }    fetchAllUsers()  }, [])
  return (
    <>
      <h1>List of Users</h1>

      <p>No users found</p>
    </>
  )
}

Finally, I'm just displaying the error if one exists, and displaying the users if they loaded.

App.js
import React, { useState, useEffect } from 'react'

export const App = () => {
  const [users, setUsers] = useState(null)
  const [error, setError] = useState(null)

  useEffect(() => {
    // ...
  }, [])

  return (
    <>
      <h1>List of Users</h1>
      {error && <div>{error}</div>}      {users ? (        <ul>          {users.map((user) => (            <li key={user.id}>{user.name}</li>          ))}        </ul>      ) : (        <p>No users found</p>      )}    </>
  )
}

Okay, now the whole application is complete and we can write the tests. You can build to ensure everything is working properly first.

Build:

npm run build

Output:

> webpack

asset main.js 145 KiB [compared for emit] [minimized] (name: main) 1 related asset
...
webpack 5.75.0 compiled successfully in 4035 ms

I use http-server in the dist folder to test the output of an application quickly.

Writing the Tests

You may not have needed all the above context and just want to see the tests. I wrote about it all since it's an up-to-date (for now) example of everything you need to get started, which can be nicer than opening twenty tabs in StackOverflow and seeing answers from 2016.

Now to get started writing the tests. I opted to put them in App.test.js, but of course there are differing opinions on where tests should live (which I discussed a bit in the React Architecture article). I'm just putting them in a tests folder for the sake of this example.

To set up, we'll use render and screen from the React Testing Library. As implied by the names, render is responsible for rendering your app to the JS Dom, and screen allows you to interact with it and see what's there.

I'm putting everything in a describe() block for App, and making sure fetchMock resets between each test.

tests/App.test.js
import React from 'react'
import { render, screen } from '@testing-library/react'

import { App } from 'src/App.js'

describe('App', () => {
  beforeEach(() => {
    fetchMock.resetMocks()
  })

  test('renders users when API call succeeds', async () => {})

  test('renders error when API call fails', async () => {})
})
When the API call succeeds

First, I'll write the test for when the API call succeeds.

Using fetchMock, I'll mock the resolved value of the JSON Placeholder /users API with a list of fake users.

const fakeUsers = [
  { id: 1, name: 'Joe' },
  { id: 2, name: 'Tony' },
]
fetchMock.mockResolvedValue({ status: 200, json: jest.fn(() => fakeUsers) })

Now, what we want is to see what happens after the successful fetch, which is the users displayed and the "No users found" message not to be there.

This can be done using a combination of waitFor and getBy, which uses act behind the scenes to wait for the event to happen:

await waitFor(() => {
  expect(screen.getByText('Joe')).toBeInTheDocument()
})

However, the findBy query is a combination of waitFor and getBy, so we can simplify that even more into a one liner:

expect(await screen.findByText('Joe')).toBeInTheDocument()

So here's our code to mock the fetch, render the App, ensure the data is rendered, and ensure nothing we don't want to see is visible:

tests/App.test.js
test('renders users when API call succeeds', async () => {
  const fakeUsers = [
    { id: 1, name: 'Joe' },
    { id: 2, name: 'Tony' },
  ]
  fetchMock.mockResolvedValue({ status: 200, json: jest.fn(() => fakeUsers) })

  render(<App />)

  expect(screen.getByRole('heading')).toHaveTextContent('List of Users')

  expect(await screen.findByText('Joe')).toBeInTheDocument()
  expect(await screen.findByText('Tony')).toBeInTheDocument()

  expect(screen.queryByText('No users found')).not.toBeInTheDocument()
})
When the API call fails

Using what we've learned in the previous test, writing the next test is pretty easy. Instead of resolving a successful API call, we'll have the API throw an error and ensure the error is visible.

tests/App.test.js
test('renders error when API call fails', async () => {
  fetchMock.mockReject(() => Promise.reject('API error'))

  render(<App />)

  expect(await screen.findByText('Something went wrong!')).toBeInTheDocument()
  expect(await screen.findByText('No users found')).toBeInTheDocument()
})

Now that both tests are written, we just need to run them.

Running the tests

Using the jest command, we can run the tests. You can also add the --coverage flag to see if the tests are catching everything.

npm test
> jest --coverage

 PASS  tests/App.test.js
  App
    ✓ renders users when API call succeeds (66 ms)
    ✓ renders error when API call fails (6 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |     100 |      100 |     100 |     100 |
 App.js   |     100 |      100 |     100 |     100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.91 s

As we can see, the tests passed with 100% coverage, so we get that green dopamine hit.

Note: I don't necessarily think having 100% test coverage is particularly meaningful, nor that component tests are the most important type of test. Generally, I think end-to-end tests are the most essential, but unit/component tests can be helpful to supplement them.

Conclusion

Well, there you have it. React, React Testing Library, Jest, and Webpack, all working in harmony in (almost) TYOOL 2023. Hopefully this helps someone struggling to figure out how to test useEffect or get their environment set up!

https://taniarascia.com/how-to-test-useeffect-api-call/
Using Path Matching in React Router
Recently on a project I was working on I noticed every page was importing in order to display the unique page title, like this: For an app…
Show full content

Recently on a project I was working on I noticed every page was importing react-helmet in order to display the unique page title, like this:

import React from 'react'
import { Helmet } from 'react-helmet'

export const UsersPage = () => {
  return (
    <div>
      <Helmet>
        <title>TaniaCorp | Users</title>
      </Helmet>

      <h1>My Component</h1>
    </div>
  )
}

For an app that has thirty pages, that's the same code repeated thirty times, so it would be ideal to be able to define it all in one place. This aligns with the Don't Hardcode Repetitive Markup guideline in the Tao of React. Anywhere we can reduce repetition with a little clear abstraction is worth looking into.

So for routes such as a UsersPage for listing all users or UserEditorPage for a single user, you would probably have a path pattern like /users and /users/:user_id.

<PrivateRoute exact path="/users" component={UsersPage} />
<PrivateRoute exact path="/users/:user_id" component={UserEditorPage} />

I thought maybe you could just use useLocation() in the React Router package to get the path (pattern), then make an object to make /users/:user_id match to the title User Editor. However, useLocation() does not give you the pattern the route is based on - it gives you the pathname in the URL. In the case of /users/:user_id, the pathname might look like /users/123.

One way of working with this data might be to do some URL splitting or maybe write some regex, but I assumed there was a simpler way.

React Router has a function called matchPath which can be used to determine if the current path matches a pattern. Here's an example of the API for that:

const pathname = '/users/123'
const pattern = '/users/:user_id'

matchPath(pathname, { path: pattern, exact: true })

With that, you can build an map of keys (patterns) to values (titles) and use matchPath to find the current match.

import { matchPath } from 'react-router'

const pageTitles = {
  '/users': 'Users',
  '/users/:user_id': 'User Editor',
}

export const getPageTitleFromUrl = (pathname) => {
  const currentPageTitle = Object.keys(pageTitles).find((key) => {
    if (matchPath(pathname, { path: key, exact: true })) {
      return true
    }

    return false
  })

  return pageTitles[currentPageTitle]
}

Note: I'm using React Router v5 for the examples, it seems that the order of pattern and pathname have been reversed in v6 for some reason. This seems like the type of move PHP would never make.

Ultimately, you could end up pulling this information in the PrivateRoute component your application probably has.

import React from 'react'
import { Helmet } from 'react-helmet'

import { getPageTitleFromUrl } from './helpers'

const PrivateRoute = ({ component: Component, ...rest }) => {
  const location = useLocation()
  const pageTitle = getPageTitleFromUrl(location.pathname)

  // Not including any irrelevant authentication code
  return (
    <>
      <Helmet>
        <title>TaniaCorp | {pageTitle}</title>
      </Helmet>
      <Route component={Component} {...rest} />
    </>
  )
}

So what's the best part about writing this article? As I got to this point, I realized if you're using a route wrapper anyway, you could just pass the title directly into the PrivateRoute and use it as a prop instead of all the unnecessary matchPath code.

Well, sometimes the simplest solution evades us, and you never know what might seem obvious to you when you take a second look at what you've written after stepping away for a while.

That's the REAL moral of the story.

https://taniarascia.com/path-matching-in-react-router/
Tending to My Digital Garden
Hi there. It's December 2022. How are you doing? This has been a slow year for me. I'm going to write a little post to let you know what I…
Show full content

Hi there. It's December 2022. How are you doing?

This has been a slow year for me. I'm going to write a little post to let you know what I've been up to, how I'm feeling, and what I'm planning going forward.

Also, I drew this RAM ram, so I decided to make it the new mascot of my site. 🐏

Treating My Work as Precious

There are a lot of reasons contributing to less activity from me, but I think one of the major ones is that I've been treating all my blog posts like they have to be precious. They're not posts, I'll think, they're articles. It's not a blog, it's the writing section of my site. It feels weird to have a short, low-effort post about how I'm doing, what I'm doing, or something personal next to a massive diatribe about the event loop or everything you might want to know about CSS fundamentals.

It's weird that I feel weird about that, considering it's my blog, so what else could or should it consist of aside from whatever I want to put on it? There are no sponsors, no affiliates, no guest posts, I'm not selling you anything, I have no set writing schedule, it's just whatever I find interesting and whatever I'm working on. If that ends up being scales on the chromatic accordion, or a list of shortcuts I use on Photoshop, or a blog roll of personal website designs I enjoy, or how my day is going, that should be completely fine. Look at kottke, nothing is too precious for his site.

So I'm writing this post to give myself permission to put whatever I want on the site. One thing that held me back is that I have around 13,000 newsletter subscribers, and I felt like I didn't want to bother them, so I was neither posting nor sharing anything that I didn't deem big or important enough. This means that months go by without me sending out a single email.

But then I had the thought - so many companies spam me all day with content I don't want in the attempt to get my money. It doesn't matter if I uncheck the "send me promotional materials" option, I'll get those emails. No matter how hard I work towards having a perfect inbox that doesn't get any spam where I only get personal emails, important emails, and a few newsletters I actually sign up for, this impersonal, useless content still manages to sneak through.

That doesn't mean I shouldn't respect your inbox, but once again - it's not quite as precious as I'm making it out to be. I'm not sending out an emergency push notification to your phone, I'm sending you an email that you can read at your leisure. Personally, I'd rather get an email in my inbox from a random, genuine person online talking about whatever interests them on a semi-regular basis then the vast majority of emails I actually receive. Anyone who isn't interested is free to unsubscribe. It's not a big deal. What do you think?

Working with Burnout

Another thing that affects me is burnout, which is honestly something I've been dealing with on and off for a long time. I have a lot of methods for managing it. Mostly I try to do my best to not look at code after 5pm on a workday, and I usually don't look at code at all on weekends.

Like anyone else in the field, I get imposter syndrome. The fact that I keep my coding to a minimum during non-work hours means I'm not familiar with the latest and greatest in my field. I've never tried out Rome. I don't really know what Astro, Vite, and Quik are. I have no experience with Kafka. I don't know if webpack is old and crusty and I should be using something else for my bundling. Since I'm focused on front end at my current job, my SQL, Node, and backend architecture skills are not progressing and are being replaced with other things, like a recipe for Chicken Masala or how to play Waltz No. 2 by Shostakovich. Not to say that I haven't picked up anything new - I made keyboardaccordion.com with Svelte this year, for example, a language I hadn't previously used for anything.

One thing I have is a confidence that I can jump into a new project or language or framework and learn whatever is necessary and relevant for me to succeed. I live by the mantra of "You don't need to know anything". You'll figure it out when you need to know it. This is not necessarily something that is tested in coding interviews, however.

Anyway, it's easy for me to feel like I haven't accomplished much. Only seven articles for the whole year? Hardly any creative accomplishments? But then I look back over eight years of several side projects, and mostly consistent writing for both myself and companies in addition to having a full-time job and trying to live a life and be a person, and maybe it's not so bad. Maybe I can be proud of what I've done, and not be afraid to do something small or something new.

I wish taking a sabbatical was more commonplace. Actually, every time I think about it, I just think about the mess that dealing with insurance would be, and then I put it off. Hitting that reset button wouldn't be a bad idea, though.

Personal Issues

The truth is this year did not get off to a good start, and it's been a little hard to bounce back from. As I wrote about earlier this year, one of my lifelong best friends died suddenly and unexpectedly. Most of my personal life and activities revolved around friendship with a few specific people, from vacations to video games to dinner nights and game nights and birthdays and everything else, so it was a big shock to my system in both personal grief and understanding my place in the world.

I've had to grapple with and think about death and existence far more than I wanted to or expected to, and at some points it has been particularly rough. The fact that I've been able to pretty much work full-time without much of a disruption and continue to be productive is actually quite surprising to me.

Oddly enough, I was sick more during the first half of this year than any other time in my life, culminating in a really horrible bout of Covid that lasted over a week and kept me bedridden. After that one, I haven't gotten sick again, so I'm hoping to go a nice, long while without getting sick. I'm sure a lot of the sicknesses had to do with all the horrible diseases and viruses floating around right now, but it certainly wasn't helping or being helped by my mood and situation, and it kept compounding.

On another note, romantic endeavors have been a wash this year. But I'm feeling much better about next year.

How Things Are Going

This post has a pretty moody vibe going so far, but actually I'm feeling really good right now. Things have really been going well, particularly in the last few months, and I think it's important to take note of that. Keeping track of what you're doing when things are going well can be helpful to look at when things aren't going so well.

So a few things going on lately:

  • Moving to a new apartment and building a new interior space
  • Quitting and deleting the game I was playing far too much (Heroes of the Storm)
  • Cooking at home instead of ordering out basically every day
  • Running a mile every morning (also got a Fitbit)
  • Buying a Wacom tablet and a Photoshop subscription and beginning to draw again
  • Buying a new style of accordion and learning some new songs
  • Attempting to have house plants for the very first time
  • Organizing and moving my notes over to Obsidian
  • Organizing and attending social events

I don't want to get too detailed and into everything I've done this year, since I like doing the year in review to see what I've been up to, but this is a pretty good summary of what's going on lately.

I recently completed my battlestation, and I updated it to be a dual desk - one side for my WFH life, and one side for play.

battlestation

Featured in this image is the PC I built during the early days of the pandemic.

Oh yeah, I've also been tweaking this website design incessantly. It's funny, by the time I make a redesign post, I've already tweaked the design so much that my before pictures from the new design aren't the same as the after pictures from the old design. Playing around with my design is kind of a procrastination tactic for me though, and I'm REALLY happy with it right now, so hopefully I can focus more on making stuff and less on the medium.

So there's just been a lot going on lately, between art, music, creativity, health, fitness, organization, and life. Well, I'd love to hear your thoughts, and I'll be planning on making more little posts going forward.

https://taniarascia.com/digital-gardening/
Simplifying Drag and Drop (Lists and Nested Lists)
I always forget how to use any drag and drop library until I have to use it again. Currently, the one I've been working with is , the…
Show full content

I always forget how to use any drag and drop library until I have to use it again. Currently, the one I've been working with is react-beautiful-dnd, the library created by Atlassian for products such as Jira. I'll admit, I'm not usually the biggest fan of Atlassian products, but it's a good library for working with drag and drop, particularly for usage with lists.

react-beautiful-dnd does tend to get a bit verbose, especially when working with nested lists, so I moved a lot of the details to reusable components and made some demos to share with you.

Goals and Demos

I made two demos for this article. In the first, I create Drag and Drop components that simplify usage with the react-beautiful-dnd library. In the second, I use those components again for a nested drag and drop, in which you can drag categories or drag items between categories.

These demos have almost no styling whatsoever - I'm more interested in showing the raw functionality with as little style as possible, so don't pay too much attention to how pretty it is(n't).

Simple Drag and Drop List

First, we'll make a simple drag and drop list.

You'll need a reorder function for getting the new order of whatever has been dragged and dropped:

helpers.js
export const reorder = (list, startIndex, endIndex) => {
  const result = Array.from(list)
  const [removed] = result.splice(startIndex, 1)
  result.splice(endIndex, 0, removed)

  return result
}

The DragDropContext, Draggable, and Droppable components work to create the list with draggable items, so I made a ListComponent that handles a complete draggable/droppable list:

ListComponent.js
import React, { useState } from 'react'
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'
import { reorder } from './helpers.js'

export const ListComponent = () => {
  const [items, setItems] = useState([
    { id: 'abc', name: 'First' },
    { id: 'def', name: 'Second' },
  ])

  const handleDragEnd = (result) => {
    const { source, destination } = result

    if (!destination) {
      return
    }

    const reorderedItems = reorder(items, source.index, destination.index)

    setItems(reorderedItems)
  }

  return (
    <DragDropContext onDragEnd={handleDragEnd}>
      <Droppable droppableId="droppable-id">
        {(provided, snapshot) => {
          return (
            <div ref={provided.innerRef} {...provided.droppableProps}>
              {items.map((item, index) => {
                return (
                  <Draggable draggableId={item.id} index={index}>
                    {(provided, snapshot) => {
                      return (
                        <div ref={provided.innerRef} {...provided.draggableProps}>
                          <div {...provided.dragHandleProps}>Drag handle</div>
                          <div>{item.name}</div>
                        </div>
                      )
                    }}
                  </Draggable>
                )
              })}
              {provided.placeholder}
            </div>
          )
        }}
      </Droppable>
    </DragDropContext>
  )
}

As you can see, even in the simplest example it gets nested like 12 levels deep. Good thing we use 2-space indentation in JavaScript! We also have multiple provided and snapshot to deal with, and when it gets nested you now have four of them, and multiple placeholder, so it starts to get really confusing.

I made it slightly worse by not using implicit returns, but personally I really dislike using implicit returns because it makes it harder to debug (read: console.log()) things.

In any case, I like to break these out into their own components: Drag and Drop.

Drop

The Drop contains the Droppable, and passes the references and props along. I've also added a type which will be used with the nested drag and drop, but can be ignored for now.

Drop.js
import { Droppable } from 'react-beautiful-dnd'

export const Drop = ({ id, type, ...props }) => {
  return (
    <Droppable droppableId={id} type={type}>
      {(provided) => {
        return (
          <div ref={provided.innerRef} {...provided.droppableProps} {...props}>
            {props.children}
            {provided.placeholder}
          </div>
        )
      }}
    </Droppable>
  )
}
Drag

The Drag handles the drag handle (odd sentence), which is often represented by an icon of six dots, but for this simplified example, it's just a div with some text.

Drag.js
import { Draggable } from 'react-beautiful-dnd'

export const Drag = ({ id, index, ...props }) => {
  return (
    <Draggable draggableId={id} index={index}>
      {(provided, snapshot) => {
        return (
          <div ref={provided.innerRef} {...provided.draggableProps} {...props}>
            <div {...provided.dragHandleProps}>Drag handle</div>
            {props.children}
          </div>
        )
      }}
    </Draggable>
  )
}
Putting it together

I find it useful to make an index.js component that exports everything.

index.js
import { DragDropContext as DragAndDrop } from 'react-beautiful-dnd'
import { Drag } from './Drag'
import { Drop } from './Drop'

export { DragAndDrop, Drag, Drop }
Usage

Now, instead of all that nonsense from the beginning of the article, you can just use Drag and Drop:

import React, { useState } from 'react'
import { DragAndDrop, Drag, Drop } from './drag-and-drop'
import { reorder } from './helpers.js'

export const ListComponent = () => {
  const [items, setItems] = useState([
    /* ... */
  ])

  const handleDragEnd = (result) => {
    // ...
  }

  return (
    <DragAndDrop onDragEnd={handleDragEnd}>
      <Drop id="droppable-id">
        {items.map((item, index) => {
          return (
            <Drag key={item.id} id={item.id} index={index}>
              <div>{item.name}</div>
            </Drag>
          )
        })}
      </Drop>
    </DragAndDrop>
  )
}

You can see the whole thing working together on the demo.

Much easier to read, and you can make the draggable look however you want as long as you're using the same drag handle style. (I only added the most basic amount of styling to differentiate the elements.)

Nested Drag and Drop List with Categories

These components can also be used for nested drag and drop. The most important thing is to add a type for nested drag and drop to differentiate between dropping within the same outer category, or dropping between categories.

To start, instead of just having one items array, we're going to have a categories array, and each object within that array will contain items.

const categories = [
  {
    id: 'q101',
    name: 'Category 1',
    items: [
      { id: 'abc', name: 'First' },
      { id: 'def', name: 'Second' },
    ],
  },
  {
    id: 'wkqx',
    name: 'Category 2',
    items: [
      { id: 'ghi', name: 'Third' },
      { id: 'jkl', name: 'Fourth' },
    ],
  },
]

The handleDragEnd function gets a lot more complicated, because now we need to handle three things:

  • Dragging and dropping categories
  • Dragging and dropping items within the same category
  • Dragging and dropping items into a different category

To do this, we'll gather the droppableId of the source and destination, which will be the category ids. Then it's either a simple reorder, or the source needs to be added to the new destination. In the new handleDragEnd function below, you can see all three of these situations handled:

const handleDragEnd = (result) => {
  const { type, source, destination } = result

  if (!destination) return

  const sourceCategoryId = source.droppableId
  const destinationCategoryId = destination.droppableId

  // Reordering items
  if (type === 'droppable-item') {
    // If reordering within the same category
    if (sourceCategoryId === destinationCategoryId) {
      const updatedOrder = reorder(
        categories.find((category) => category.id === sourceCategoryId).items,
        source.index,
        destination.index
      )
      const updatedCategories = categories.map((category) =>
        category.id !== sourceCategoryId ? category : { ...category, items: updatedOrder }
      )

      setCategories(updatedCategories)
    } else {
      // Dragging to a different category
      const sourceOrder = categories.find((category) => category.id === sourceCategoryId).items
      const destinationOrder = categories.find(
        (category) => category.id === destinationCategoryId
      ).items

      const [removed] = sourceOrder.splice(source.index, 1)
      destinationOrder.splice(destination.index, 0, removed)

      destinationOrder[removed] = sourceOrder[removed]
      delete sourceOrder[removed]

      const updatedCategories = categories.map((category) =>
        category.id === sourceCategoryId
          ? { ...category, items: sourceOrder }
          : category.id === destinationCategoryId
          ? { ...category, items: destinationOrder }
          : category
      )

      setCategories(updatedCategories)
    }
  }

  // Reordering categories
  if (type === 'droppable-category') {
    const updatedCategories = reorder(categories, source.index, destination.index)

    setCategories(updatedCategories)
  }
}

Now you can see the category has a droppable-category type, and the item has a droppable-item type, which differentiates them. We now have two layers of <Drop> and <Drag> components.

NestedListComponent.js
import React, { useState } from 'react'
import { DragAndDrop, Drag, Drop } from './drag-and-drop'
import { reorder } from './helpers.js'

export const NestedListComponent = () => {
  const [categories, setCategories] = useState([
    /* ... */
  ])

  const handleDragEnd = (result) => {
    /* ... */
  }

  return (
    <DragAndDrop onDragEnd={handleDragEnd}>
      <Drop id="droppable" type="droppable-category">
        {categories.map((category, categoryIndex) => {
          return (
            <Drag key={category.id} id={category.id} index={categoryIndex}>
              <div>
                <h2>{category.name}</h2>

                <Drop key={category.id} id={category.id} type="droppable-item">
                  {category.items.map((item, index) => {
                    return (
                      <Drag key={item.id} id={item.id} index={index}>
                        <div>{item.name}</div>
                      </Drag>
                    )
                  })}
                </Drop>
              </div>
            </Drag>
          )
        })}
      </Drop>
    </DragAndDrop>
  )
}

I won't even show you what this looks like without the Drag and Drop components.

In the nested drag and drop demo, you can test out dragging between categories, dragging within a category, and dragging a category itself, including all the items it contains.

Conclusion

Drag and drop can get pretty unwieldy, especially when using the react-beautiful-dnd library for nested lists. By creating reusable components, you can make it much easier to use and understand.

I always try to see if I can tame my work when it seems like it's getting out of control, and this is just one example. Hope you enjoyed the article and demos!

https://taniarascia.com/simplifying-drag-and-drop/
Memories of Josh
I'm just going to write some memories of Josh. The mundane, everyday things. The stuff I'll slowly, inevitably forget, because I'm not sure…
Show full content

I'm just going to write some memories of Josh. The mundane, everyday things. The stuff I'll slowly, inevitably forget, because I'm not sure what else to write but I want to at least have a place where I can remember a bit. And maybe a place to share a bit, so others can know what I knew and it lasts a little longer.

josh1

Josh would almost always win every game. And it didn't matter if it was a strategy game, board game, Chess, Mario Kart, or anything else - he would win, and everyone knew he would win, and everyone would gang up against him, and he'd still win.

josh2

Here he is beating me at Chess. I was trying to get into Chess at this time, so I was mildly infuriated every time he won. After any game, he'd extend his hand to shake and he'd say, "Good game!" in a very genuine matter, and I'd be one step short of flipping the table.

For the last five or six years, Josh, Joe, and I would play a strategy game online together at least once a week. We knew each other's moves and habits and half the time we'd know how the game would end just a few minutes in. After years of practicing, I was able to get a good amount of wins in, but I never beat Josh one-on-one, and I believe the last tally had me losing 13 times in a row.

Star Trek was always playing in the background from his microphone.

josh3

Josh was competitive and wanted to win, but he was always friendly and always wanting everyone to feel welcome, and never acted toxic in any way. Whenever someone was new to a game, he would help them along and show them all the rules and encourage them. Even though sometimes when he'd say, "You almost had me that time!" it wasn't close to true, but he made you believe that it was.

josh4

We pretended to have invented some sort of game here when only coins and paper were available. We thought it was funny to take a picture looking deep in strategic thought over this nonsensical game.

Josh could not for the life of him pronounce "caterpillar" or "cinnamon" properly. It was always "capertiller" and "cimmanon".

Josh was a vegetarian and had been for many years. I remember him saying he'd never try octopus because they're way too intelligent, and at some point he wasn't eating any meat at all. He said the meal he missed the most was fried chicken, so sometimes I'd make vegetarian Nashville chicken sandwiches with breaded, fried cauliflower as an alternative. He didn't make a big deal about being a vegetarian. I heard him cite concerns about the environment being one reason, but he was above all an animal lover. His first job back in high school was a working at a veterinarian clinic as a helper, but he couldn't do it for long because it hurt him too much to put down the animals.

josh6

He even cared about the bugs and spiders so much that he'd always prevent anyone from killing them and would gently place them outside. The only time I ever saw him allow it was that time we stayed at a cabin that was practically raining ticks.

josh7

This was Josh's eccentric billionaire picture.

Josh was a Disney princess. There was always a big bag of peanuts next to the backdoor at his house and every day he and Katie would go outside with a handful and the squirrels would run up and grab them. They had names for all the squirrels.

He had a cactus named "General Poke". I tried to give him one of my wilted succulents so he could fix it in the cactus nursery but it was beyond saving.

He could put his leg all the way behind his head somehow.

One of his favorite random videos to show at parties was "Double King", which never fails to baffle.

josh9

We met in 7th grade, when I was 12. We had some classes together, and there was overlap between different friend groups; the band geeks and art nerds and gaming dorks. He made fun of the way I run in gym class, and I made fun of the fact that he spelled "does" as "dose" in an essay. He was truly terrible at spelling, and that never changed. My first memory of him coming over to my house as a kid involved him being a pyro and using a lighter to burn the little bits of stuffing that were spilling out of a couch that was in the outdoor screenhouse. We called it couch cheese because they looked like little orange chunks of cheddar.

He loved making popcorn, but he would always forget about it and burn it. One time I think he burned it five times in a row. One year I got him something to make popcorn in the microwave, but I think he still insisted on making it on the stove. I made up for it by repeatedly burning bagels when I was trying to make them in the broiler because I didn't have a toaster.

josh10

We were together the first time we ever got drunk.

josh10a

He was down to go to music festivals or the Renaissance Faire or whatever random event was going on, but he wouldn't go to something if he wasn't interested at all.

josh11

There was a period of my life where I really wanted to do something different - be independent, adventurous, travel - but I was still too nervous to do it by myself. I asked Josh if he'd be interested in going on a two week backpacking trip to Scandinavia with me, and he did.

josh12

The real purpose was to visit the ABBA museum in Sweden, of course.

josh13

We took Vespa rides around city. At one point we accidentally got on a bridge to the highway and just white-knuckled it on the 50cc scooters until we got back to the side streets, but it was quite the view.

josh14

He was always chipper and waking up early in the hostels and making conversations with the other people. Meanwhile, I was sleeping in.

We went to Denmark and ended up in Christiania, a small little hippie town with no laws or government. We met a group of women from Turkey there and talked to them about the political turmoil that was active at the time.

josh15

I told Josh about surströmming, a Swedish fermented fish delicacy that is known for having one of the most putrid scents in the world. He found a can and really wanted us to try it, but sadly I refused.

josh16

I discovered some mountain halfway up Sweden, eight hours away from Stockholm, so we decided to take a long train to get there, except it dropped us off on a road in the middle of nowhere. We had to walk several miles until we arrived at a tiny town. The town had a hotel of sorts with only three rooms, except the door was locked. We walked around and there was nothing else in the town and no other people, until we reached the other end where there was a small convenience store. We walked up to the cashier, and he handed us a white bag with a key in it without speaking a word. Looking at each other, we shrugged and went back to the hotel, used the key, and sure enough we were able to get in. Not a single other soul was at this building the whole time we were there.

The next day, we climbed the mountain and I remember he was proud.

josh17 josh18

We would get together often to make meals. Sometimes we'd try making ravioli from scratch, or pizza from scratch, or we'd make veggie chili. He always had a recipe.

josh20a

We tried making Swedish meatballs soon after returning from Sweden. Since I lived in a very small, dingy studio at the time, we had to use a curtain and a shower door to make a table.

josh19

If I went in the kitchen to do something, Josh would always come in right away. "How can I help?" then he would grab some veggies and start chopping, or start gathering ingredients for dough, or do any other task to help. He would bring the "Josh" wine and we would buy the fancy cheese. The last time I saw him, one of the cheeses was still unopened, but he said we'd just save it for next time.

josh20

I remember one time we set off the fire alarm trying to make a pizza, and the dog hid in a closet for the next few hours.

josh21

He was always fidgeting with something. A lighter, a twist tie from bread, a wrapper, a switch blade. Sometimes I'd get him little brain puzzles for his birthday, but Katie would be the one who ended up solving them.

josh22

Josh has been the best part of the worst days of my life. When I was 22 and married, I knew I couldn't stay a moment longer in my situation, and I called Josh and said I needed to be picked up right now. He and Keith arrived in his minivan and that night we moved everything I would take with me to the next part of my life.

There were a few times we would have a bottle of scotch and talk all night.

I knew all the stories he would tell, but he was so expressive and good at telling them that I was always happy to hear them again. He would always tell the story with his whole body.

josh28 josh23

We went camping a lot. Every summer, we would take at least one camping trip. We had just started to do winter cabin trips as well. He would always buy a can of SpaghettiOs, as we had some sort of joke about the SpaghettiO Gods cursing us if we didn't perform the ritual with it.

josh24 josh25

One of his favorite camping stories/making fun of me stories is the time I had too much rum and coke and, as they were trying to stoke the fire on the cold night, I said "I'm done with this!" and poured the rest of it on the fire. Followed by, "...I'm cold".

josh27

Josh brought the joys of Hammer-Schlagen to camping, the game where you find a suitable tree stump, some nails, and a hammer, and some combination of drinking, throwing the hammer in the air, and hitting a nail.

josh29

Josh was talented at many things. He didn't sing or dance often, but I could tell from karaoke that he was good at it. He was especially good at anything that required fine details like wire craft. He started drawing for the first time and was already picking up perspective and shading. Most of all, he was great at writing and being imaginative. He always carried around notebooks that he was writing his stories and adventures in. I have only played tabletop RPGs a handful of times, but Josh was always the game master, which was always fun because he would do voices for all the different characters.

His favorite celebrity was Paul Rudd, or Nicolas Cage, depending on the day.

Josh was there during one of the worst moments I can think of in my life, a particularly bad panic and anxiety situation, being reassuring and talking me through.

josh30

He always used a GPS to get around, even just to get to the gas station.

He had a lot of nicknames, but my favorite one was calling him "JORSCH!!!" in Goofy's voice.

josh31

Josh and Katie's relationship was the gold standard of what a relationship could be, as far as I have ever seen in my life. They seemed to have found the perfect balance of being best friends who are engaged and interested in each other's lives, while still having plenty of their own hobbies, activities, and interests.

They reminded me of what's possible.

josh32

Josh didn't like tea or coffee, and he called tea "dirty water". But due to Picard, he would make an exception for Earl Grey, hot.

josh33

For a year or two, Josh would have a big "R" on his hand, written in sharpie. I asked him what it was about, and he said it was for him to remember. I never knew what it was that he wanted to remember.

I can still hear him saying, "Fifteen percent!! ...better make it twenty."

josh34

On Friday, March 4th, Josh and Katie came over after work, and we walked to the grocery store to buy ingredients for dinner. He talked about some things going on at work, how he was working on a chemical plant that was using an extremely minuscule amount of materials, and all the tubes were tiny. I said that shouldn't be a problem for him since he was good with small details. We bought some "Pastrami on Rye" beer for some reason, but I got a 4-pack of Boddington's as a backup. We got back and made a little naan flatbread with roasted vegetables and goat cheese. Josh helped chop up the vegetables as always.

We just sat around talking for a few hours. He tried moving the bowl of water for my cat, saying they were more likely to drink it if it wasn't right next to the food source. He put on some ASMR videos on YouTube. They went home when it got late.

On Saturday, March 5th, I texted him about a silly, weird dream I had, and we talked about dreams for a bit.

On Sunday, March 6th, I woke up feeling pretty great. I was taking a walk outside and sent a group message that we should all play a few rounds of HOTS. We played a few, then all went about our day. I had a family party, then out to dinner to plan a vacation with Keith and Heather. At dinner, we got the call.

josh35

One day, I had a nice camera and it was a nice day outside, so I took some pictures. It still feels a bit surreal to me that a picture from that day is the one they chose for his obituary.

I haven't even scratched the surface of the pictures and memories.

I don't really know what else to say. It still doesn't actually feel real to me. It immediately felt like too much to handle, too overwhelming, some scary feeling that was too big to be acknowledged, so I put it away somewhere in my mind that I wouldn't have to deal with it. Although I was in the house when the paramedics came to take him away, I focused all my attention on the carpet or my shoes or anything else so I wouldn't have to hear or see anything.

When I went to sleep that night, Josh was there, and I said "Josh!!" and ran over and gave him a hug. I asked him what happened, and we sat down and he told me about how it was all just a big misunderstanding. Then I woke up. My brain couldn't make sense of it, so this was the trick it played on me. Now he appears in my dreams most nights, but something is always wrong or off.

So as far as my brain is concerned, he's just somewhere that never has service, and he's just too busy to ever hang out anymore. But we're still telling stories about him, about that time he did donuts in the parking lot when we were kids, or how he always fell asleep in the back of the car, or belting out "I Need a Hero", or how he was always chewing on ice so you'd randomly hear a loud cronch while watching a movie.

The only emotions I register are annoyance and anger. What's he so busy with? Why can't he ever come over for dinner anymore? Why isn't he coming camping this year?

This life is fragile, more fragile than we can ever imagine. You can try to keep yourself safe, but it's possible to just die suddenly of no known cause. There's nothing I wish I would have said or wish I would have done differently. We always knew how much we mattered to each other.

So all I can do is keep the memories alive, and in remembering he feels more alive than ever.

He was always the best of us.

josh8

May his memory be a blessing.

https://taniarascia.com/josh/
Redesign: Version 6.0
Once again, I've redesigned my website. Every now and then I get bored of the way it looks, or I notice a lot of people cloning it and feel…
Show full content

Once again, I've redesigned my website. Every now and then I get bored of the way it looks, or I notice a lot of people cloning it and feel like I have to make something more unique again. The last time I changed it was September of last year, so I almost made it a full year without a complete redesign. Of course, if you look at the version I blogged about in the last redesign post vs. the one I had right before updating, you'll see it changed a lot. I tend to never be completely satisfied and tweak it a lot. It's always my hope that I can tweak less and write more! But this is my procrastination hobby, I guess.

Here are the last few redesigns:

New Layout

Here's how the new layout looks:

Front page:

v61

Blog page:

v62

Post page:

v63

Light theme:

v64

Old Layout

Here's how the latest (let's call it version 5.1) looked before I updated it.

Front page:

v51

Blog page:

v52

Post page:

v53

Sepia theme:

v54

Light theme:

v55

My original plan was to make the site have a bunch of 80s themes. I wanted it to look like an old VHS, which is a design I really love. I couldn't quite get it right then just decided to stick some of the colors in, but here's an idea of what that was starting to look like.

v6progress

Notably, I added category and tags lists, and I added a sidebar to the posts page. I think the fonts and sizes and colors are cleaner now, even though it was already pretty good. I feel like this design opens it up for me to add more stuff to the about page, like links to personal posts that I might like to highlight. A sidebar is missing from that page, but I haven't decided what to put there yet.

Do you like it? Hate it? I had it sitting there, half way done, for quite a while, and felt like I couldn't make any posts while it was pending, so I finished it up while I was just laying here at home sick with Covid. Now I have some ideas for posts to write and I can do that without being distracted.

https://taniarascia.com/redesign-version-6/
How To Set Up a GraphQL API Server in Node.js
In An Introduction to GraphQL, you learned that GraphQL is an open-source query language and runtime for APIs created to solve issues that…
Show full content

In An Introduction to GraphQL, you learned that GraphQL is an open-source query language and runtime for APIs created to solve issues that are often experienced with traditional REST API systems.

A good way to begin understanding how all the pieces of GraphQL fit together is to make a GraphQL API server. Although Apollo GraphQL is a popular commercial GraphQL implementation favored by many large companies, it is not a prerequisite for making your own GraphQL API server.

In this tutorial, you will make an Express API server in Node.js that serves up a GraphQL endpoint. You will also build a GraphQL schema based on the GraphQL type system, including operations, such as queries and mutations, and resolver functions to generate responses for any requests. You will also use the GraphiQL integrated development environment (IDE) to explore and debug your schema and query the GraphQL API from a client.

Prerequisites

To follow this tutorial, you will need:

Setting Up an Express HTTP Server

The first step is to set up an Express server, which you can do before writing any GraphQL code.

In a new project, you will install express and cors with the npm install command:

npm install express cors

Express will be the framework for your server. It is a web application framework for Node.js designed for building APIs. The CORS package, which is Cross-Origin Resource Sharing middleware, will allow you to easily access this server from a browser.

You can also install Nodemon as a dev dependency:

npm install -D nodemon

Nodemon is a tool that helps develop Node-based applications by automatically restarting the application when file changes in the directory are detected.

Installing these packages will have created node_modules and package.json with two dependencies and one dev dependency listed.

Using nano or your favorite text editor, open package.json for editing, which will look something like this:

package.json
{
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.17.3"
  },
  "devDependencies": {
    "nodemon": "^2.0.15"
  }
}

There are a few more fields you will add at this point. To package.json, make the following highlighted changes:

package.json
{
  "main": "server.js",
  "scripts": {
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.17.3"
  },
  "devDependencies": {
    "nodemon": "^2.0.15"
  },
  "type": "module"
}

You will be creating a file for the server at server.js, so you make main point to server.js. This will ensure that npm start starts the server.

To make it easier to develop on the server, you also create a script called "dev" that will run nodemon server.js.

Finally, you add a type of module to ensure you can use import statements throughout the code instead of using the default CommonJS require.

Save and close the file when you're done.

Next, create a file called server.js. In it, you will create a simple Express server, listen on port 4000, and send a request saying Hello, GraphQL!. To set this up, add the following lines to your new file:

server.js
import express from 'express'
import cors from 'cors'

const app = express()
const port = 4000

app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

app.get('/', (request, response) => {
  response.send('Hello, GraphQL!')
})

app.listen(port, () => {
  console.log(`Running a server at http://localhost:${port}`)
})

This code block creates a basic HTTP server with Express. By invoking the express function, you create an Express application. After setting up a few essential settings for CORS and JSON, you will define what should be sent with a GET request to the root (/) using app.get('/'). Finally, use app.listen() to define the port the API server should be listening on.

Save and close the file when you're done.

Now you can run the command to start the Node server:

npm run dev

If you visit http://localhost:4000 in a browser or run a curl http://localhost:4000 command, you will see it return Hello, GraphQL!, indicating that the Express server is running. At this point, you can begin adding code to serve up a GraphQL endpoint.

Setting Up GraphQL HTTP Server Middleware

In this section, you will begin integrating the GraphQL schema into the basic Express server. You will do so by defining a schema, resolvers, and connecting to a data store.

To begin integrating GraphQL into the Express server, you will install three packages: graphql, express-graphql, and @graphql-tools/schema. Run the following command:

npm install graphql@14 express-graphql @graphql-tools/schema
  • graphql: the JavaScript reference implementation for GraphQL.
  • express-graphql: HTTP server middleware for GraphQL.
  • @graphql-tools/schema: a set of utilities for faster GraphQL development.

You can import these packages in the server.js file by adding the highlighted lines:

server.js
import express from 'express'
import cors from 'cors'
import { graphqlHTTP } from 'express-graphql'
import { makeExecutableSchema } from '@graphql-tools/schema'

...

The next step is to create an executable GraphQL schema.

To avoid the overhead of setting up a database, you can use an in-memory store for the data the GraphQL server will query. You can create a data object with the values your database would have. Add the highlighted lines to your file:

server.js
import express from 'express'
import cors from 'cors'
import { graphqlHTTP } from 'express-graphql'
import { makeExecutableSchema } from '@graphql-tools/schema'

const data = {
  warriors: [
    { id: '001', name: 'Jaime' },
    { id: '002', name: 'Jorah' },
  ],
}

...

The data structure here represents a database table called warriors that has two rows, represented by the Jaime and Jorah entries.

Note: Using a real data store is outside of the scope of this tutorial. Accessing and manipulating data in a GraphQL server is performed through the reducers. This can be done by manually connecting to the database, through an ORM like Prisma. Asynchronous resolvers make this possible through the context of a resolver. For the rest of this tutorial, we will use the data variable to represent datastore values.

With your packages installed and some data in place, you will now create a schema, which defines the API by describing the data available to be queried.

GraphQL Schema

Now that you have some basic data, you can begin making a rudimentary schema for an API to get the minimum amount of code necessary to begin using a GraphQL endpoint. This schema is intended to replicate something that might be used for a fantasy RPG game, in which there are characters who have roles such as warriors, wizards, and healers. This example is meant to be open-ended so you can add as much or as little as you want, such as spells and weapons.

A GraphQL schema relies on a type system. There are some built-in types, and you can also create your own type. For this example, you will create a new type called Warrior, and give it two fields: id and name.

type Warrior {
  id: ID!
  name: String!
}

The id has an ID type, and the name has a String type. These are both built-in scalars, or primitive types. The exclamation point (!) means the field is non-nullable, and a value will be required for any instance of this type.

The only additional piece of information you need to get started is a base Query type, which is the entry point to the GraphQL query. We will define warriors as an array of Warrior types.

type Query {
  warriors: [Warrior]
}

With these two types, you have a valid schema that can be used in the GraphQL HTTP middleware. Ultimately, the schema you define here will be passed into the makeExecutableSchema function provided by graphql-tools as typeDefs. The two properties passed into an object on the makeExecutableSchema function will be as follows:

  • typeDefs: a GraphQL schema language string.
  • resolvers: functions that are called to execute a field and produce a value.

In server.js, after importing the dependencies, create a typeDefs variable and assign the GraphQL schema as a string, as shown here:

server.js
...

const data = {
  warriors: [
    { id: '001', name: 'Jaime' },
    { id: '002', name: 'Jorah' },
  ],
}

const typeDefs = `
type Warrior {
  id: ID!
  name: String!
}

type Query {
  warriors: [Warrior]
}
`

...

Now you have your data set as well as your schema defined, as data and typeDefs, respectively. Next, you'll create resolvers so the API knows what to do with incoming requests.

GraphQL Resolver Functions

Resolvers are a collection of functions that generate a response for the GraphQL server. Each resolver function has four parameters:

  • obj: The parent object, which is not necessary to use here since it is already the root, or top-level object.
  • args: Any GraphQL arguments provided to the field.
  • context: State shared between all resolvers, often a database connection.
  • info: Additional information.

In this case, you will make a resolver for the root Query type and return a value for warriors.

To get started with this example server, pass the in-memory data store from earlier in this section by adding the highlighted lines to server.js:

server.js
...

const typeDefs = `
type Warrior {
  id: ID!
  name: String!
}

type Query {
  warriors: [Warrior]
}
`

const resolvers = {
  Query: {
    warriors: (obj, args, context, info) => context.warriors,
  },
}

...

The entry point into the GraphQL server will be through the root Query type on the resolvers. You have now added one resolver function, called warriors, which will return warriors from context. context is where your database entry point will be contained, and for this specific implementation, it will be the data variable that contains your in-memory data store.

Each individual resolver function has four parameters: obj, args, context, and info. The most useful and relevant parameter to our schema right now is context, which is an object shared by the resolvers. It is often used as the connection between the GraphQL server and a database.

Finally, with the typeDefs and resolvers all set, you have enough information to create an executable schema. Add the highlighted lines to your file:

server.js
...

const resolvers = {
  Query: {
    warriors: (obj, args, context, info) => context.warriors,
  },
}

const executableSchema = makeExecutableSchema({
  typeDefs,
  resolvers,
})

...

The makeExecutableSchema function creates a complete schema that you can pass into the GraphQL endpoint.

Now replace the default root endpoint that is currently returning Hello, GraphQL! with the following /graphql endpoint by adding the highlighted lines:

server.js
...

const executableSchema = makeExecutableSchema({
  typeDefs,
  resolvers,
})

app.use(
  '/graphql',
  graphqlHTTP({
    schema: executableSchema,
    context: data,
    graphiql: true,
  })
)

...

The convention is that a GraphQL server will use the /graphql endpoint. Using the graphqlHTTP middleware requires passing in the schema and a context, which in this case, is your mock data store.

You now have everything necessary to begin serving the endpoint. Your server.js code should look like this:

server.js
import express from 'express'
import cors from 'cors'
import { graphqlHTTP } from 'express-graphql'
import { makeExecutableSchema } from '@graphql-tools/schema'

const app = express()
const port = 4000

// In-memory data store
const data = {
  warriors: [
    { id: '001', name: 'Jaime' },
    { id: '002', name: 'Jorah' },
  ],
}

// Schema
const typeDefs = `
type Warrior {
  id: ID!
  name: String!
}

type Query {
  warriors: [Warrior]
}
`

// Resolver for warriors
const resolvers = {
  Query: {
    warriors: (obj, args, context) => context.warriors,
  },
}

const executableSchema = makeExecutableSchema({
  typeDefs,
  resolvers,
})

app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

// Entrypoint
app.use(
  '/graphql',
  graphqlHTTP({
    schema: executableSchema,
    context: data,
    graphiql: true,
  })
)

app.listen(port, () => {
  console.log(`Running a server at http://localhost:${port}`)
})

Save and close the file when you're done.

Now you should be able to go to http://localhost:4000/graphql and explore your schema using the GraphiQL IDE.

Your GraphQL API is now complete based on the schema and resolvers you created in this section. In the next section, you'll use the GraphiQL IDE to help you debug and understand your schema.

Using the GraphiQL IDE

Since you applied the graphiql option as true to the GraphQL middleware, you have access to the GraphiQL integrated development environment (IDE). If you visited the GraphQL endpoint in a browser window, you'll find yourself in GraphiQL.

GraphiQL is an in-browser tool for writing, validating, and testing GraphQL queries. Now you can test out your GraphQL server to ensure it's returning the correct data.

Make a query for warriors, requesting the id and name properties. In your browser, add the following lines to the left pane of GraphiQL:

{
  warriors {
    id
    name
  }
}

Submit the query by pressing the Play arrow on the top left, and you should see the return value in JSON on the right-hand side:

{
  "data": {
    "warriors": [
      { "id": "001", "name": "Jaime" },
      { "id": "002", "name": "Jorah" }
    ]
  }
}

If you remove one of the fields in the query, you will see the return value change accordingly. For example, if you only want to retrieve the name field, you can write the query like this:

{
  warriors {
    name
  }
}

And now your response will look like this:

{
  "data": {
    "warriors": [{ "name": "Jaime" }, { "name": "Jorah" }]
  }
}

The ability to query only the fields you need is one of the powerful aspects of GraphQL and is what makes it a client-driven language.

Back in GraphiQL, if you click on Docs all the way to the right, it will expand a sidebar labeled Documentation Explorer. From that sidebar, you can click through the documentation to view your schema in more detail.

Now your API is complete and you've explored how to use it from GraphiQL. The next step will be to make actual requests from a client to your GraphQL API.

Querying the GraphQL API from a Client

Just like with REST APIs, a client can communicate with a GraphQL API by making HTTP requests over the network. Since you can use built-in browser APIs like fetch to make network requests, you can also use fetch to query GraphQL.

For a very basic example, create an HTML skeleton in an index.html file with a <pre> tag:

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <title>GraphQL Client</title>
  </head>

  <pre><!-- data will be displayed here --></pre>

  <body>
    <script>
      // Add query here
    </script>
  </body>
</html>

In the script tag, make an asynchronous function that sends a POST request to the GraphQL API:

index.html
...
<body>
    <script>
      async function queryGraphQLServer() {
        const response = await fetch('http://localhost:4000/graphql', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            query: '{ warriors { name } }',
          }),
        })
        const data = await response.json()

        // Append data to the pre tag
        const pre = document.querySelector('pre')
        pre.textContent = JSON.stringify(data, null, 2) // Pretty-print the JSON
      }

      queryGraphQLServer()
    </script>
  </body>
...

The Content-Type header must be set to application/json, and the query must be passed in the body as a string. The script will call the function to make the request, and set the response in the pre tag.

Here is the full index.html code.

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <title>GraphQL</title>
  </head>

  <pre></pre>

  <body>
    <script>
      async function queryGraphQLServer() {
        const response = await fetch('http://localhost:4000/graphql', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            query: '{ warriors { name } }',
          }),
        })
        const data = await response.json()

        const pre = document.querySelector('pre')
        pre.textContent = JSON.stringify(data, null, 2) // Pretty-print the JSON
      }

      queryGraphQLServer()
    </script>
  </body>
</html>

Save and close the file when you're done.

Now when you view the index.html file in a browser, you will see an outgoing network request to the http://localhost:4000/graphql endpoint, which will return a 200 with the data. You can view this network request by opening Developer Tools and navigating to the Network tab.

If your request went through and you got a 200 response with the data from the GraphQL API, congratulations! You made your first GraphQL API server.

Conclusion

In this tutorial, you made a GraphQL API server using the Express framework in Node.js. The GraphQL server consists of a single /graphql endpoint that can handle incoming requests to query the data store. Your API had a schema with the base Query type, a custom Warrior type, and a resolver to fetch the proper data for those types.

Hopefully, this article helped demystify GraphQL and opens up new ideas and possibilities of what can be accomplished with GraphQL. Many tools exist that can help with the more complex aspects of working with GraphQL, such as authentication, security, and caching, but learning how to set up an API server in the simplest way possible should help you understand the essentials of GraphQL.

This tutorial is part of our How To Manage Data with GraphQL series, which covers the basics of using GraphQL.

This article was originally written for DigitalOcean.

https://taniarascia.com/graphql-server-node/
Building an JavaScript Keyboard Accordion (the Musical Kind)
It's been a while since I've written anything due to some personal concerns that I might write about later, but don't worry, I'm still…
Show full content

It's been a while since I've written anything due to some personal concerns that I might write about later, but don't worry, I'm still around and I'm still coding. Recently, I went to Texas and bought a three-row diatonic button accordion. Diatonic accordions are popular for a lot of different types of folk music, which is generally learned by ear. This is good for me, because I don't really know how to read music anyway.

The accordion has 34 buttons on the treble side and 12 buttons on the bass side. Unlike a piano accordion, which has the same logical, chromatic layout as a piano, the diatonic accordion just has a bunch of buttons and I didn't really know where to start. Also, every note is different whether you're pulling the bellows out or pushing them in, so there are actually 68 notes on the treble side (albeit some are repeated). Also, as I'm sure you might be aware, accordions are loud. Very loud. In order to not piss off my neighbors too much, and to learn how the layout of this box works, I decided to make a little web app.

kascreenshot

That web app is KeyboardAccordion.com, which like everything else I create in my free time is open source. I noticed that there are just enough keys on a computer keyboard to correspond to the accordion layout, and they're arranged in a similar pattern. With this, I can keep track of the notes, scales, and chords and start figuring out how to put it all together.

Here's what one of the accordions looks like:

corona2

I decided to make this app in Svelte, because I've used React and Vue professionally but have no experience with Svelte whatsoever and wanted to know what everyone loves about it.

Web Audio API

KeyboardAccordion.com only has one dependency, and that's Svelte. Everything else is done using plain JavaScript and the built-in browser Web Audio API. I'd never really used the Web Audio API before, so I figured out what I needed to to get this working.

The first thing I did was create an AudioContext and attach a GainNode, which controls the volume.

const audio = new (window.AudioContext || window.webkitAudioContext)()
const gainNode = audio.createGain()
gainNode.gain.value = 0.1
gainNode.connect(audio.destination)

As I was figuring everything out, I was experimenting with making new AudioContext for every note because I was trying to fade out the sound, but then I kept realizing that after 50 notes, the app would stop working. Fifty is apparently the limit for the browser, so it's better to just make one AudioContext for the entire app.

I'm using waves with the Audio API and not using any sort of audio sample, and I used the OscillatorNode to make each note. There are various types of waves you can use - square, triangle, sine, or sawtooth, which all have a different type of sound. I went with the sawtooth for this app because it worked out the best. Square makes an extremely loud, chiptune-esque sound like an NES which is kind of nice in its own way. Sine and triangle were a bit more subdued but if you don't fade the sound out properly, it makes a really unpleasant kind of cutting sound due to how your ear reacts when a wave gets cut off.

Waveforms

waveform

So for each note, I'd make an oscillator, set the wave type, set the frequency, and start it. Here's an example using 440, which is a standard tuning for "A".

const oscillator = audio.createOscillator()
oscillator.type = 'sawtooth'
oscillator.connect(gainNode)
oscillator.frequency.value = 440
oscillator.start()

If you do that, the note will just play until infinity, so you have to make sure you stop the oscillator when you want the note to end.

oscillator.stop()

For me, this meant event listeners on the DOM that would listen for a keypress event to see if any button was pressed, and a keyup event to determine when any button was no longer being pressed. In Svelte, that's handled by putting event listeners on svelte:body.

<svelte:body
  on:keypress="{handleKeyPressNote}"
  on:keyup="{handleKeyUpNote}"
  on:mouseup="{handleClearAllNotes}"
/>

So that's really everything there is to the Web Audio API itself when it comes to setting up the app - creating an AudioContext, adding a Gain, and starting/stopping an Oscillator for each note.

You could paste this into the console and it'll play a note. You'll have to either refresh or type oscillator.stop() to make it stop.

const audio = new (window.AudioContext || window.webkitAudioContext)()
const gainNode = audio.createGain()
gainNode.gain.value = 0.1
gainNode.connect(audio.destination)

const oscillator = audio.createOscillator()
oscillator.type = 'sawtooth'
oscillator.connect(gainNode)
oscillator.frequency.value = 440
oscillator.start()
Data Structure

I had to figure out how I wanted to lay out the data structure for this application. First of all, if I'm going to be using the Web Audio API with frequencies directly, I had to collect all of them.

Frequencies

Here's a nice map of notes to frequencies with all 12 notes and 8-9 octaves for each note, so I can use A[4] to get the 440 frequency.

tone
export const tone = {
  C: [16.35, 32.7, 65.41, 130.81, 261.63, 523.25, 1046.5, 2093.0, 4186.01],
  Db: [17.32, 34.65, 69.3, 138.59, 277.18, 554.37, 1108.73, 2217.46, 4434.92],
  D: [18.35, 36.71, 73.42, 146.83, 293.66, 587.33, 1174.66, 2349.32, 4698.64],
  Eb: [19.45, 38.89, 77.78, 155.56, 311.13, 622.25, 1244.51, 2489.02, 4978.03],
  E: [20.6, 41.2, 82.41, 164.81, 329.63, 659.26, 1318.51, 2637.02],
  F: [21.83, 43.65, 87.31, 174.61, 349.23, 698.46, 1396.91, 2793.83],
  Gb: [23.12, 46.25, 92.5, 185.0, 369.99, 739.99, 1479.98, 2959.96],
  G: [24.5, 49.0, 98.0, 196.0, 392.0, 783.99, 1567.98, 3135.96],
  Ab: [25.96, 51.91, 103.83, 207.65, 415.3, 830.61, 1661.22, 3322.44],
  A: [27.5, 55.0, 110.0, 220.0, 440.0, 880.0, 1760.0, 3520.0],
  Bb: [29.14, 58.27, 116.54, 233.08, 466.16, 932.33, 1864.66, 3729.31],
  B: [30.87, 61.74, 123.47, 246.94, 493.88, 987.77, 1975.53, 3951.07],
}
Button layout

Figuring out exactly how to arrange all the buttons into a data stucture took a couple of tries for me. The data that had to be captured was:

  • The row on the accordion
  • The column on the accordion
  • The direction of the bellows (push or pull)
  • The name and frequency of the note at that row, column, and direction

This means that there are different combinations for all three sets of these things. I decided to make an id that corresponds to each possible combination, such as 1-1-pull being row 1, column 1, direction pull.

This way, I could create an array that holds the data for any note that is currently being played. If you press the button to reverse the bellows, it would take all the currently playing notes and reverse them, thus changing 1-1-pull and 1-2-pull to 1-1-push and 1-2-push.

So ultimately I had an object that contained the data for all three treble rows like so:

layout
const layout = {
  one: [],
  two: [],
  three: [],
}

My particular accordion is tuned to FB♭Eb, meaning the first row is tuned to F, the second row is tuned to B♭, and the third row is tuned to E♭. The example for the first row looks like this:

layout
const layout = {
  one: [
    // Pull
    { id: '1-1-pull', name: 'D♭', frequency: tone.Db[4] },
    { id: '1-2-pull', name: 'G', frequency: tone.G[3] },
    { id: '1-3-pull', name: 'B♭', frequency: tone.Bb[3] },
    { id: '1-4-pull', name: 'D', frequency: tone.D[4] },
    { id: '1-5-pull', name: 'E', frequency: tone.E[4] },
    { id: '1-6-pull', name: 'G', frequency: tone.G[4] },
    { id: '1-7-pull', name: 'B♭', frequency: tone.Bb[4] },
    { id: '1-8-pull', name: 'D', frequency: tone.D[5] },
    { id: '1-9-pull', name: 'E', frequency: tone.E[5] },
    { id: '1-10-pull', name: 'G', frequency: tone.G[5] },
    // Push
    { id: '1-1-push', name: 'B', frequency: tone.B[3] },
    { id: '1-2-push', name: 'F', frequency: tone.F[3] },
    { id: '1-3-push', name: 'A', frequency: tone.A[3] },
    { id: '1-4-push', name: 'C', frequency: tone.C[4] },
    { id: '1-5-push', name: 'F', frequency: tone.F[4] },
    { id: '1-6-push', name: 'A', frequency: tone.A[4] },
    { id: '1-7-push', name: 'C', frequency: tone.C[5] },
    { id: '1-8-push', name: 'F', frequency: tone.F[5] },
    { id: '1-9-push', name: 'A', frequency: tone.A[5] },
    { id: '1-10-push', name: 'C', frequency: tone.C[6] },
  ],
  two: [
    // ...etc
  ],
}

There are notes 1 through 10 in row one, and each one has a name and frequency associated with it. Repeating this for two and three, I now have all 68 notes on the treble side.

Keyboard layout

Now I had to map each key on the keyboard to a row and column of the accordion. Direction doesn't matter here, since z will correspond to both 01-01-push and 01-01-pull.

keyMap
export const keyMap = {
  z: { row: 1, column: 1 },
  x: { row: 1, column: 2 },
  c: { row: 1, column: 3 },
  v: { row: 1, column: 4 },
  b: { row: 1, column: 5 },
  n: { row: 1, column: 6 },
  m: { row: 1, column: 7 },
  ',': { row: 1, column: 8 },
  '.': { row: 1, column: 9 },
  '/': { row: 1, column: 10 },
  a: { row: 2, column: 1 },
  s: { row: 2, column: 2 },
  d: { row: 2, column: 3 },
  f: { row: 2, column: 4 },
  g: { row: 2, column: 5 },
  // ...etc
}

Now I have all the keys from z to /, a to ', and w to [ mapped out. Very auspicious that the computer keyboard and accordion keyboard are so similar.

Pressing keys, playing notes

As you might recall, I have an event listener on the entire page listening for the key press event. Any key press event that happens will go through this function.

First, it has to check both lowercase and uppercase keys in case shift or caps lock are pressed, otherwise the keys won't work at all. Then, if you're pressing the button to toggle the bellows (which I made q), it has to handle that separately. Otherwise, it will check the keyMap, and if one exists, it will find the corresponding id by checking the current direction and getting the row and column from the keymap.

handleKeyPressNote
let activeButtonIdMap = {}

function handleKeyPressNote(e) {
  const key = `${e.key}`.toLowerCase() || e.key // handle caps lock

  if (key === toggleBellows) {
    handleToggleBellows('push')
    return
  }

  const buttonMapData = keyMap[key]

  if (buttonMapData) {
    const { row, column } = buttonMapData
    const id = `${row}-${column}-${direction}`

    if (!activeButtonIdMap[id]) {
      const { oscillator } = playTone(id)

      activeButtonIdMap[id] = { oscillator, ...buttonIdMap[id] }
    }
  }
}

The way I'm tracking each currently playing note is putting them in the activeButtonIdMap object. In Svelte, in order to update a variable you just reassign it, so instead of what you might do in React with useState:

React
const [activeButtonIdMap, setActiveButtonIdMap] = useState({})

const App = () => {
  function handleKeyPressNote() {
    setActiveButtonIdMap(newButtonIdMap)
  }
}

You have to declare it as a let and reassign it:

Svelte
let activeButtonIdMap = {}

function handleKeyPressNote() {
  activeButtonIdMap = newButtonIdMap
}

This was mostly easier, except when all I wanted to do was delete a key from the object. As far as I could tell, Svelte only rerenders when a variable is reassigned, so just mutating some value within wasn't enough and I had to clone it, mutate it, the reassign it. This is what I did in the handleKeyUpNote function.

handleKeyUpNote
function handleKeyUpNote(e) {
  const key = `${e.key}`.toLowerCase() || e.key

  if (key === toggleBellows) {
    handleToggleBellows('pull')
    return
  }

  const buttonMapData = keyMap[key]

  if (buttonMapData) {
    const { row, column } = buttonMapData
    const id = `${row}-${column}-${direction}`

    if (activeButtonIdMap[id]) {
      const { oscillator } = activeButtonIdMap[id]
      oscillator.stop()
      // Must be reassigned in Svelte
      const newActiveButtonIdMap = { ...activeButtonIdMap }
      delete newActiveButtonIdMap[id]
      activeButtonIdMap = newActiveButtonIdMap
    }
  }
}

Maybe someone knows a better way to delete an item from an object in Svelte, but this is the best I could come up with.

I also made a few functions that will play through the scales, starting with F, B♭ and E♭ being the main diatonic keys of the accordion, but there are more options. To play the scales, I simply looped through all the ids that correspond to the notes in the scale and used a JavaScript "sleep" command of 600ms between each note.

Rendering

Now that I have all the data structures set up and the JavaScript, I just need to render all the buttons. Svelte has #each blocks for looping logic, so I just looped through the three rows of buttons and rendered a circle for each button.

<div class="accordion-layout">
  {#each rows as row}
    <div class="row {row}">
      {#each layout[row].filter(({ id }) => id.includes(direction)) as button}
        <div
          class={`circle ${activeButtonIdMap[button.id] ? 'active' : ''} ${direction} `}
          id={button.id}
          on:mousedown={handleClickNote(button.id)}
        >
          {button.name}
        </div>
      {/each}
    </div>
  {/each}
</div>

Each circle has its own mousedown event so you can click on them in addition to using the keyboard, but I didn't put the mouseup event on the circle itself. This is because if you move your mouse somewhere else before lifting it up, it won't correctly determine the mouseup and the note will play forever.

And of course, I just used plain CSS because I don't usually feel like anything fancier is necessary for small projects.

.circle {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  height: 60px;
  width: 60px;
  margin-bottom: 10px;
  background: linear-gradient(to bottom, white, #e7e7e7);
  box-shadow: 0px 6px rgba(255, 255, 255, 0.4);
  color: #222;
  font-weight: 600;
  cursor: pointer;
}

.circle:hover {
  background: white;
  box-shadow: 0px 6px rgba(255, 255, 255, 0.3);
  cursor: pointer;
}

.circle.pull:active,
.circle.pull.active {
  background: linear-gradient(to bottom, var(--green), #56ea7b);
  box-shadow: 0px 6px rgba(255, 255, 255, 0.2);
}

.circle.push:active,
.circle.push.active {
  background: linear-gradient(to bottom, var(--red), #f15050);
  box-shadow: 0px 6px rgba(255, 255, 255, 0.2);
  color: white;
}
Conclusion

I hope you liked my write-up for the Keyboard Accordion app! Of course, the full code is available on GitHub.

There are a few little bugs here and there, such as if you use keyboard shortcuts while also pressing other keys, it will get stuck on a note forever. I'm sure if you try to find more bugs you'll be able to.

This app was fun to make, I learned how to use both Svelte and the Web Audio API, and it's helping me and hopefully some other afficionados to understand the squeezebox a little better. Maybe it'll inspire you to build your own little online instrument, or make an app for one of your hobbies. The best part about coding is that you can make anything you want!

https://taniarascia.com/musical-instrument-web-audio-api/
Creating a Schema-Based Form System
View the Source or Demo for the schema-based form system described in this article. Working with forms on the front end is tedious and…
Show full content

View the Source or Demo for the schema-based form system described in this article.

Working with forms on the front end is tedious and repetitive. If you don't have a good system set up, it can involve a lot of copy and pasting. If you have a bad abstraction, it can be much worse.

I've worked with some nightmare systems that were significantly worse than just manually writing all the form logic, error handling, validation, dirty state, etc. But nonetheless, taking care of all that can really start to add up and take a lot of time.

Wouldn't it be nice if we could do something like this: define the schema of a form and pass it into a component that takes care of all the common form needs...

Schema-based form example
const ExampleForm = () => {
  const schema = [
    { name: 'name', label: 'Name', componentType: 'text', required: true },
    {
      name: 'class',
      label: 'Class',
      componentType: 'select',
      options: [
        { value: 'ranger', label: 'Ranger' },
        { value: 'wizard', label: 'Wizard' },
      ],
    },
  ]

  return <AdvancedForm schema={schema} onSubmit={handleSubmit} />
}

Instead of writing all this: handling the values, errors, validation, and components manually?

Manual form example
const ExampleForm = () => {
  const [formValues, setFormValues] = useState({})
  const [errors, setErrors] = useState({})
  const [touched, setTouched] = useState({})

  const handleSubmit = () => {
    /* ... */
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="name">Name (required)</label>
      <input
        type="text"
        id="name"
        name="name"
        value={value}
        onChange={() => {
          /* ... */
        }}
      />

      <label htmlFor="class">Class</label>
      <select
        id="class"
        name="class"
        value={value}
        onChange={() => {
          /* ... */
        }}
      >
        <option key="ranger" value="ranger">
          Ranger
        </option>
        <option key="wizard" value="wizard">
          Wizard
        </option>
      </select>

      <button type="submit">Submit</button>
    </form>
  )
}

I made an example GitHub repo and demo of such a system. This follows the rule from the Tao of React - Do not hardcode markup.

Of course, disclaimer time, this isn't a production-ready repository with tests and edge cases accounted for and every type of form component includes and bindings for different frameworks - it's just an example that you can use to learn from or build from.

The simple example I made does include a text field, select, checkbox, radio group, text area, as well as conditional fields. To make it useful for the real world, you could integrate it with your UI framework of choice (such as Material UI or Semantic UI) if you're using one, or you can add support for multi-selects, checkbox groups, asyncronous responses, and much more!

form screenshot

Technology

Although I'm not using a UI framework in the example to handle form component styles, I am using a library to handle values and form submission - Formik. It's an extremely widely-used tool for working with forms that takes care of much of the annoying stuff, while still being simple under the hood and not bringing in theb complexity of Redux, MobX, observables, or anything else - just simple React state.

Additionally, Yup can be used for validation, in order to avoid writing all the same common regex over and over again.

Using Formik in the project makes it easy to abstract it out and allow us to pass some simple schema in.

Form System

Based on the example I showed above, you can see that a schema property will get passed in, as well as the onSubmit handler. This is basically enough for any "create" form, and for an "edit" form, I've also added an initialValues prop that can pre-populate the form with any existing values.

I'm using the <Formik> component (reference) from Formik to build this system. It contains render props that contain all the values in the entire form at all times, as well as some helpers like isValid or isSubmitting, which let you know the current state of the form.

AdvancedForm.js
import { Formik, Field } from 'formik'

import { getInitialValues, getDefaultValues, getValidationSchema } from './helpers'

export const AdvancedForm = ({ schema, onSubmit, initialValues, ...props }) => {
  const defaultValues = getDefaultValues(schema)
  const validationSchema = getValidationSchema(schema)

  return (
    <Formik
      initialValues={getInitialValues(defaultValues, initialValues)}
      validationSchema={validationSchema}
      onSubmit={onSubmit}
      validateOnMount
      {...props}
    >
      {({ handleSubmit, isSubmitting, isValid, values }) => {
        return (
          <form onSubmit={handleSubmit}>
            {/* Form schema components will go here */}
            <button type="submit" disabled={!isValid || isSubmitting}>
              Submit
            </button>
          </form>
        )
      }}
    </Formik>
  )
}
Get default values for the schema

Before I go into creating and rendering the components, we want to make sure we get default values for all the items in the schema.

This means if the schema looks like this:

const schema = [
  { name: 'name', label: 'Name', type: 'text' },
  { name: 'is_manager', label: 'Is Manager', type: 'checkbox' },
]

We want a default values object that looks like this:

const defaultValues = { name: '', is_manager: false }

We can accomplish that by running through the schema and reducing it based on component type:

helpers/getDefaultValues
export const getDefaultValues = (schema) => {
  return schema.reduce((acc, val) => {
    let defaultValue

    switch (val.componentType) {
      case 'text':
      case 'textarea':
      case 'select':
      case 'radioGroup':
        defaultValue = ''
        break
      case 'checkbox':
        defaultValue = false
        break
      default:
        defaultValue = ''
    }

    return { ...acc, [val.name]: val.defaultValue || defaultValue }
  }, {})
}

This way, we're never passing in null or undefined values into the form, and it's always receiving the type it expects.

Get validation object for the schema

Just like the default values, we'll want a schema object. Using the Yup library, we can just pass in values, like Yup.string() for text fields, radio values, etc. and Yup.array() for something like a multi-select or checkbox group.

helpers/getValidationSchema
export const getValidationSchema = (schema) => {
  const validationObject = schema.reduce((acc, val) => {
    let validationType

    switch (val.componentType) {
      case 'text':
      case 'textarea':
      case 'select':
      case 'radioGroup':
        validationType = Yup.string()
        break
      case 'checkbox':
      default:
        validationType = null
    }

    if (val.required && validationType) {
      validationType = validationType.required(`${val.label} is required`)
    }

    return { ...acc, ...(validationType && { [val.name]: validationType }) }
  }, {})

  return Yup.object().shape(validationObject)
}

Now that I think about it, there's probably some way to use the Yup schema for both default values and validation, but I did not look further into it.

Set initial values

Now we can set the initial values - either the defaultValues by default, or a passed in initialValues if you're editing an existing form.

helpers/getInitialValues
export const getInitialValues = (defaultValues, initialValues) => {
  if (!initialValues) return defaultValues

  return { ...defaultValues, ...initialValues }
}

All the setup for the form is there now, and now you can start creating bindings for whatever form components you want in the system.

Form Components

In this article, won't go into how to create all the individual form components (you can just view the source), I'll just focus on one, but for every type of form component that you want to include in the form system, make a file for it.

forms/index.js
import { Checkbox } from './Checkbox.js'
import { Select } from './Select.js'
import { TextField } from './TextField.js'
import { TextArea } from './TextArea.js'
import { RadioGroup } from './RadioGroup.js'

export { Checkbox, Select, TextField, TextArea, RadioGroup }

Now back in the main AdvancedForm component, you can import all those components and put them in an array. When looping through the schema, you can now find the correct component to render. The component will be rendered using the Formik <Field> component (reference), which gives you access to the onChange events, touched, errors, values, etc. for each form field.

AdvancedForm.js
import { Formik, Field } from 'formik'

import { getInitialValues, getDefaultValues, getValidationSchema } from './helpers'

// Import all the form components and map them to their respective schema componentTypeimport { Checkbox, Select, TextArea, TextField, RadioGroup } from '.'const components = [  { componentType: 'text', component: TextField },  { componentType: 'textarea', component: TextArea },  { componentType: 'select', component: Select },  { componentType: 'checkbox', component: Checkbox },  { componentType: 'radioGroup', component: RadioGroup },]
export const AdvancedForm = ({ schema, onSubmit, initialValues, ...props }) => {
  const defaultValues = getDefaultValues(schema)
  const validationSchema = getValidationSchema(schema)

  return (
    <Formik
      initialValues={getInitialValues(defaultValues, initialValues)}
      validationSchema={validationSchema}
      onSubmit={onSubmit}
      validateOnMount
      {...props}
    >
      {({ handleSubmit, isSubmitting, isValid, values }) => {
        return (
          <form onSubmit={handleSubmit}>
            {schema.map(({ componentType, condition, ...formSchema }) => {              // Find the correct component from the schema based on componentType              const Component = components.find(                (component) => component.componentType === componentType              ).component              // Pass the formSchema data into the Field component              return <Field key={formSchema.name} component={Component} {...formSchema} />            })}            <button type="submit" disabled={!isValid || isSubmitting}>
              Submit
            </button>
          </form>
        )
      }}
    </Formik>
  )
}

Now you can actually make the bindings for each form type.

Text field component

All the data gets passed down to the <Field>, such as the onChange, onBlur, whether or not it has been touched or has errors, and then anything special you want to add to it. For example, the Select component would have an options prop so you can pass down a list of all the key/values for the select options.

Here is an example of a simple text field input. This could also be extended and modified to have an email type, a password type, or anything else you might want a regular input to be able to handle.

forms/TextField.js
export const TextField = ({
  label,
  field: { name, value, ...fieldProps },
  form: { touched, errors },
  required,
  ...props
}) => {
  const hasError = errors[name] && touched[name]

  return (
    <>
      <label htmlFor={name}>
        {label}
        {required && <sup className="required">*</sup>}
      </label>
      <input type="text" id={name} name={name} value={value} {...fieldProps} {...props} />
      {hasError && <small className="error">{errors[name]}</small>}
    </>
  )
}

The same code can be extended for checkboxes, radios, selects, multi-selects, radio groups, sliders, and any other form type you need.

Required

In the getValidationSchema helper, we set up default types for each field in the schema. If one of them has required: true in the schema, and nothing is entered, an error will appear that says "[label name] is required".

if (val.required && validationType) {
  validationType = validationType.required(`${val.label} is required`)
}

The way the form is set up, empty values won't start off in an error state, but if they're touched and not filled out, then the error state will appear.

Errors

You can check if an error exists by seeings if the related error exists and the field has been touched.

const hasError = errors[name] && touched[name]
Conditional Fields

I added a little bonus where you can make certain fields only appear if certain conditions are met. This example schema is set up with key, value, and operator of the condition.

There's a "Class" select, that has Ranger, Wizard, and Healer as options. If you select Wizard, then another field pops up with the Spell select.

condition: { key: 'class', value: 'wizard', operator: '=' },

Here's the whole schema:

const schema = [
  {
    name: 'class',
    label: 'Class',
    componentType: 'select',
    options: [
      { label: 'Ranger', value: 'ranger' },
      { label: 'Wizard', value: 'wizard' },
      { label: 'Healer', value: 'healer' },
    ],
  },
  {
    name: 'spell',
    label: 'Spell',
    componentType: 'select',
    options: [
      { label: 'Fire', value: 'fire' },
      { label: 'Ice', value: 'ice' },
    ],
    condition: { key: 'class', value: 'wizard', operator: '=' },
  },
]

In the advanced form, you can check for condition and render a ConditionalField wrapper around the field that will hide, show, and add default values as needed.

AdvancedForm.js
// ...

if (condition) {
  return (
    <ConditionalField
      key={formSchema.name}
      show={
        condition.operator === '='
          ? values[condition.key] === condition.value
          : values[condition.key] !== condition.value
      }
      onCollapse={() => {
        setFieldValue(formSchema.name, defaultValues[formSchema.name])
        setFieldTouched(formSchema.name, false)
      }}
      onShow={() => {
        setFieldValue(formSchema.name, defaultValues[formSchema.name])
      }}
    >
      <Field component={Component} {...formSchema} />
    </ConditionalField>
  )
}

// ...

The component is a simple conditonal gate that renders children if the conditon is met.

forms/ConditionalField.js
import { useEffect } from 'react'

export const ConditionalField = ({ show, onCollapse, onShow, children }) => {
  useEffect(() => {
    if (show) {
      onShow()
    } else {
      onCollapse()
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [show])

  return show ? children : null
}
Conclusion

So there you have it, with the combined power of Formik and Yup, you can build an abstraction to drastically reduce the amount of code you have to write overall to work with forms. The concepts outlined in this article can be extended to any component library...Material UI, Ant Design, Blueprint, Semantic UI, or just plain HTML and CSS as seen here.

The conditional code here is very simple, relying on whether certain items were selected or not in the form, but you could use something like JSON schema to extend it further. A lot more component types can be created, such as an email type that has a default email regex added to the validationObject, and a multi-select dropdown component type. You might also want to factor in asynchronous conditionals.

Hopefully this article helped you think more about defining data upfront and passing it into components as opposed to hard-coding markup and manually handling form state. There will always be some situations an abstraction doesn't handle well or at all, and in those cases you might need to manually work with your form, but a system like AdvancedForm can help in many common situations.

View the Source or Demo for the schema-based form system described in this article. Thanks for reading!

https://taniarascia.com/schema-based-form-system/
Year in Review: 2021 into 2022
Wow, here we are, the sixth installment in the New Year series of posts. This year feels almost like a lost weekend. Aside from a few small…
Show full content

Wow, here we are, the sixth installment in the New Year series of posts.

2021tania

This year feels almost like a lost weekend. Aside from a few small cabin and camping trips throughout the year, I hardly left my city or even my home. I spent much of the time relaxing, reading books, being with friends and family, playing games, and I even made a few paintings.

It's been good to focus on myself and not feel guilty about not writing enough or creating enough. It's hard to believe it, but I've been working as a developer and creating all my side projects for nearly eight years now. I've experienced major burnout in the past and that has manifested in different ways, such as quitting my job and taking a three-month long solo stint in Europe, as well as long periods where I didn't look at any code outside of work. Burnout is a hard thing to get over so I'm very careful about not over doing it.

Right now, I'm still working on trying to balance my life, my job, and my creative endeavors both in coding and otherwise.

Overall, I can say I'm content. Every day, I'm happy to be alive. I marvel at the simple fact that I can move my hands and create with them, and the fact that I can connect with other human beings. I've worked hard and it has paid off in my professional life, and I'm proud of that.

Here's a couple of cool things I discovered this year:

  • Inside - Bo Burnham - Being a fan of Bo Burnham's previous comedy specials, I clicked on Inside the instant I saw it pop up on Netflix. I felt completely captivated from the first to the last instant, totally glued to the screen. There's nothing more to say about Inside that hasn't already been said, and it's extremely polarizing - everyone either loves it or hates it. For me, it's the only pandemic-related piece of pop culture I want or need.

  • Get Back - The Beatles Documentary - Like many, I discovered The Beatles at some point in my youth and became obsessed with them. Once this has happened, no matter how long you've gone without listening to The Beatles or thinking about them, anything can set you off and start the obsession all over again. Having recently watched this documentary directed by Peter Jackson about the Let It Be sessions, Abbey Road is back to turning on my record player, and I'm listening to many songs with a new appreciation. I've also been exploring and enjoying Paul McCartney's solo albums. Temporary Secretary might be the most interesting one...

A few more - Miracle Musical, Michael Kiwanuka, and Lord Huron.

  • Children of Time - Adrian Tchaikovsky - I've had mixed luck with just picking up a random book and seeing how it goes. With The Expanse series, which I picked up off the shelf randomly a few years and didn't know that it was popular and had a TV show, it was an amazing choice and just what I was looking for. The last book in the series came out recently and brought it to a close. I picked up a lot of books that I was less than satisfied with, but Children of Time really surprised me. It didn't have the amazing character development I crave from reading The Expanse or A Song of Ice and Fire, but it has a very interesting, unique, and compelling story.

A few more - Demon Haunted World (Carl Sagan), Sapiens.

As for resolutions? I wouldn't say I have any resolution technically, but first and foremost, I want to focus on working on myself, overcoming my own issues and understanding myself better. My priority is still spending time with my friends and family, appreciating the people close to me and enjoying the time we have together. I would like to write more, particularly more non-technical writing. I'd like to record more songs, a favorite hobby of mine that I haven't done for several years. I'd like to continue focusing on my health, working out and paying attention to what I eat. Mostly, I want to keep doing what I'm doing.

I wrote 7 technical articles

I haven't written as much this year as in previous years. However, I'm proud of the quality of the articles I've written.

I ended up writing about one technical article every two months. The articles I'm writing are more focused and specific, so I think they reach a smaller audience than before. I hoped that I might write more non-technical articles by adding a new section, but I only wrote one this year.

  • Integration Tests with Jest, Supertest, Knex, and Objection in TypeScript - this article is a little misleading, in that it's a lot more useful and interesting than the title suggests, at least to me. Testing is kind of a boring topic, but I was proud of getting the full integration test suite set up for a TypeScript API. The more interesting part is the TypeScript API itself, using Express, Knex, and Objection ORM.

  • How and When to Use Context in React with Hooks - I often write articles for my own reference, and the original article I wrote on React Context was out of date, since it relied on class components. I wrote this one to give a real world example of why you might use context, when Redux might be better, and how to use it.

  • Using OAuth with PKCE Authorization Flow (Proof Key for Code Exchange) - the most up-to-date secure way to handle OAuth authentication for a web or mobile app is using the PKCE flow. This article eplains the different flows, and how to set up the code challenge/code verifier in JavaScript for PKCE.

  • React Architecture: How to Structure and Organize a React Application - React architecture is a topic that's surprisingly hard to find good resources on. In the past I set it up the way I had seen other people do it, but when I discovered a domain-based approach, I decided to write about it to help anyone in the future looking for a better way.

  • Writing a Sokoban Puzzle Game in JavaScript - I had fun using what I learned on the Chip-8 project to make another little puzzle game in JavaScript. I messed it up by allowing you to push multiple blocks at once (which was actually the hardest part to program) but I left it in.

  • Front End Tables: Sorting, Filtering, and Pagination - every job I've had in development has involved tables, usually with sorting, filtering, and pagination on them. Previously I wrote an article about adding all those features to a back end API, and this article goes into doing it on the front end. There are so many (usually outdated) libraries for dealing with tables, but depending on your needs, it's often much more effective to build it yourself.

  • An Introduction to GraphQL - my only DigitalOcean article of the year, this is the first article about GraphQL. GraphQL is another one of those things that's scary until you delve into the core of it and strip away all the libraries and implementations (such as Apollo). I'm planning on writing more on the topic.

I started a new job

When 2020 began, I was working at Yum. I had started in February/March of 2020, right when the lockdowns began. I had never worked remotely before and never really wanted to, but I learned that being remote really works for me. I started a new job as a Staff Software Engineer in mid 2021, and it's been going great. When not everything is certain and life is confusing, having an everyday routine, obbligation, and a job that I enjoy and am good at adds stability to my life.

I redesigned my site

Once again, I redesigned my website, a particular hobby of mine. This was version 5.0, or so I've decided to call it. Realistically, there have been hundreds of "versions" of the website, from all the tweaks I've done over the years.

It was fun to do and make my site look like a programming IDE as well as having interactive elements like the theme color, but personally I think it's too busy and will probably redesign it once again in 2022 to try to keep it minimalist while still having a personal touch. I do enjoy the pixel art I made for it the most.

I got a kitten

Not much to say here, but definitely the biggest life change for me of the year. I got a little 10-week-old kitten in August as a family friend's cat had a litter, and named him Dimo. He's the most adorable little thing and keeps me company every day as I work.

It seems like not really a big deal, but it was for me. Making the conscious decision to take care of a life for the next 20 years was a big responsibility to me. Having a little animal around has brought a lot of joy into my life!

withdimo

I made a lot of commits

Through it all, I've been churning out code. A smattering of updates here and there on my public repo, and a very consistent output on my work repo.

2021 code

2021 code2

Well, that's it for now! Happy New Year!

https://taniarascia.com/2021-into-2022/
An Introduction to GraphQL
As web and mobile applications become more mature and complex, software engineers invent clever new ways of improving the interaction…
Show full content

As web and mobile applications become more mature and complex, software engineers invent clever new ways of improving the interaction between client and server within an application. One of the biggest paradigm shifts over the last few years in this regard has been GraphQL, an open-source query language and runtime for manipulating APIs. GraphQL was designed by Facebook in 2012 (and released publicly in 2015) to solve various weaknesses with traditional REST architecture by making a new system that is declarative, client-driven, and performant.

In this article, you will learn what GraphQL is, familiarize yourself with important terminology and concepts of GraphQL, and discover how the GraphQL specification compares with the REST architectural style.

What is GraphQL?

GraphQL stands for Graph Query Language, but unlike other query languages such as SQL (Structured Query Language), it is not a language for communicating directly with a database, but rather a language that defines a contract through which a client communicates with a API server. The GraphQL specification is an open standard that describes the rules and characteristics of the language. It also provides instructions for executing a GraphQL query.

Due to the fact that GraphQL is defined by an open-standard, there is no official implementation of GraphQL. A GraphQL implementation can be written with any programming language, integrate with any type of database, and support any client (such as mobile or web applications), as long as it follows the rules outlined in the spec. One of the most popular commercial GraphQL implementations is Apollo GraphQL, a which touts several GraphQL client and server implementations, but it is not necessary to use Apollo to use or understand GraphQL.

GraphQL Characteristics

There are several key characteristics of GraphQL design. GraphQL queries are declarative and hierarchical, and a GraphQL schema is strongly-typed and introspective.

Declarative

GraphQL queries are declarative, meaning the client will declare exactly which fields it is interested in, and the response will only include those properties.

This example GraphQL query for a hypothetical fantasy game API requests a wizard with an ID of "1", and requests the name and race fields on that object.

{
  wizard(id: "1") {
    name
    race
  }
}

The response, which is returned in JSON format, will return a data object that contains the found wizard object, with the two fields the query requested.

{
  "data": {
    "wizard": {
      "name": "Merlin",
      "race": "HUMAN"
    }
  }
}

Since a GraphQL response only gives you the exact information you want, it results in a more efficient and performant network request than alternatives that always provide a complete set of data.

Hierarchical

GraphQL queries are also hierarchical. The data returned follows the shape of the query. In this example, the query has been extended to include spells, and is requesting the name and attack fields of every spell.

{
  wizard(id: "1") {
    name
    spells {
      name
      attack
    }
  }
}

The response will now include an array of all the spell objects associated with this particular wizard. Although wizards and spells might be stored in separate database tables, they can be fetched with a single GraphQL request. (However, GraphQL is not opinionated about how the data itself is stored, so that is a presumption.)

{
  "data": {
    "wizard": {
      "name": "Merlin",
      "spells": [
        {
          "name": "Lightning Bolt",
          "attack": 2
        },
        {
          "name": "Ice Storm",
          "attack": 2
        },
        {
          "name": "Fireball",
          "attack": 3
        }
      ]
    }
  }
}
Strongly-typed

GraphQL is strongly-typed, as described by the GraphQL Type system. Types describe the capabilities of the values within a GraphQL server. The GraphQL types will be familiar to most programmers, with scalars (primitive values) like strings, booleans, and numeric integers, as well as more advanced values like objects.

This example creates a Spell Object type with fields that correspond to String and Int scalar types.

type Spell {
  name: String!
  attack: Int
  range: Int
}

A GraphQL schema is defined using the type system, which allows the server to determine whether or not a query is valid before attempting to query the data. GraphQL Validation ensures the request is syntactically correct, unambiguous, and mistake-free.

Self-documenting

The Introspection feature allows GraphQL clients and tools to query the GraphQL server for the underlying schema's shape and data. This allows for the creation of tools like GraphiQL, an in-browser IDE and playground for working with GraphQL queries, and other tools for automatically generating documentation.

For example, you can find out more about the Spell type through this introspection feature via the __schema.

{
  __schema {
    types {
      name
      kind
      description
    }
  }
}

The response will also be JSON like any other GraphQL response

{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Spell",
          "kind": "OBJECT",
          "description": "A powerful spell that a wizard can read from a scroll."
        }
      ]
    }
  }
}
Client-driven

The work of developing a GraphQL API happens on the backend, where the schema is defined and implemented. However, since all of the power of the GraphQL API is encompassed a single endpoint on the server, it is up to the client via declarative queries to decide exactly what data it needs. This empowers developers to iterate quickly, as the front end developer can continue to query the data the GraphQL API exposes without doing any additional backend work.

Architecture

GraphQL exists in the application layer between client and data. The GraphQL server describes the capabilities exposed in the API, and the client describes the requirements of the request.

Server

A GraphQL API is defined with a single endpoint, usually the /graphql endpoint, which can access the full capabilities of the GraphQL server. Since GraphQL is an application layer technology and is transport agnostic, it can be served over any protocol, but it is most commonly served over HTTP.

A GraphQL server implementation can be written with any programming language, such as the express-graphql middleware which allows you to create a GraphQL API on a Node/Express HTTP server. GraphQL is also database agnostic, and the data for the application can be stored in MySQL, PostgreSQL, MongoDB, or any other database. The data can even be supplied by an aggregation of several traditional REST API endpoints. All that matters is that the data is defined in a GraphQL schema, which defines the API by describing the data available to be queried.

Client

Requests made to a GraphQL server are called documents and consist of operations such as queries (for read requests) and mutations (for write requests).

Although there are advanced GraphQL clients, such as Apollo Client or Facebook's Relay which provide mechanisms for caching as well as additional tools, no special client is required to make a request to a GraphQL server. A simple XMLHttpRequest or fetch from a web browser is sufficient for making requests by sending a GraphQL document to a GraphQL server.

Below is an example of a fetch request to a /graphql endpoint, which passes the GraphQL document as a string in the body of the POST request.

async function fetchWizards() {
  const response = await fetch('/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: `{
    wizards {
      id
      name
    },
  }`,
    }),
  })
  const wizards = await response.json()

  return wizards
}

fetchWizards()

This will return a JSON response for the request.

{
  "data": {
    "wizards": [
      { "id": "1", "name": "Merlin" },
      { "id": "2", "name": "Gandalf" }
    ]
  }
}
GraphQL vs. REST

GraphQL and REST are not interchangeable concepts, but they solve similar problems for applications. REST stands for Representational State Transfer, and is a software architectural style for sharing data between different systems. A RESTful API is an API that adheres to the principles and constraints of REST, which include being stateless, cacheable, enforcing a separation of concerns between the client and server, and having a uniform interface, such as through URIs. GraphQL, as covered previously, is a specification for a query language and runtime for executing queries.

There are advantages and disadvantages to both systems, and both have their use in modern API development. However, GraphQL was developed to combat some perceived weaknesses with the REST system, and to create a more efficient, client-driven API.

  • Architecture - A REST API is typically defined by multiple endpoints on a server, but GraphQL exchanges data over a single endpoint. A GraphQL endpoint can return a complex graph of data that might require multiple REST queries, reducing the number of requests over the network for a single view.

  • Data fetching - A REST API returns the set of data that was determined on the server. This might be far too much data, such as if the view only requires one property from a response, or it might not be enough, such as a list endpoint that doesn't return every property that a table requires in the view. GraphQL prevents this over and under fetching of data via declarative queries.

  • Error Handling - Since it is not necessary for GraphQL to be served over HTTP, there is no specification about using HTTP response codes for errors. Typically all GraphQL endpoints will resolve with a 200 HTTP code response, and failed results will include an errors property alongside the data property in the response. RESTful APIs, on the other hand, utilize different 400 level HTTP codes for client errors and 200 level HTTP codes for successful responses.

  • Versioning - GraphQL APIs strive to be backwards compatible and avoid breaking changes, contrasting with the common REST pattern of versioning endpoints, often with a /v1 or /v2 in the URL itself to determine the version. However, it is possible to implement your own versioning with GraphQL, or version via evolution with REST, it's just less conventional.

  • Caching - Cacheability is an integral part of the REST guiding constraints. Since HTTP-based REST APIs consist of multiple endpoints using different HTTP methods, it can take advantage of existing HTTP conventions for caching and avoiding refetching resource. And since essentially every GraphQL request will be different but use the single endpoint, it cannot take advantage of any of the built-in HTTP caching mechanisms. GraphQL clients can take advantage of Global Object Identification to enable simple caching.

This list does not cover all the similarities and differences between REST and GraphQL, but summarizes many of the most critical points. Additionally, GraphQL can be used as a gateway that aggregates multiple REST endpoints or services, in which case both technologies can be used in harmony side-by-side.

Feature GraphQL REST Description GraphQL is a query language for APIs, and a server-side runtime An architectural style for designing web services Data Fetching A single HTTP endpoint that responds to deterministic queries A set of HTTP endpoints that typically return a predetermined dataset Versioning Versioning discouraged Versioning common HTTP Status Codes All responses, including errors, are typically 200 Implements HTTP Status codes Validation Built-in metadata validation Validation must be manually implemented Documentation Built-in via type system and introspection Not self-documenting, tools like OpenAPI available Caching No Yes Request Methods Queries, mutations, and subscriptions (over POST for HTTP) All HTTP methods utilized (GET, POST, PATCH, PUT, DELETE, etc) Response Content-Type JSON Any (JSON, XML, HTML, etc.) Conclusion

GraphQL is an open-source query language and runtime for APIs. GraphQL was invented by developers at Facebook to solve various issues encountered with traditional REST APIs, such as over/under fetching data and inefficient network requests, by making a client-driven, declarative query language for APIs.

While GraphQL is not an interchangeable concept with REST, they both describe different ways to manage communication between a client and a server. In this article, you learned what GraphQL is, key differences and similarities between GraphQL and REST, and how a GraphQL server exposes data to a client.

This article was originally written for DigitalOcean.

https://taniarascia.com/introduction-to-graphql/
Behind the Tutorials
Despite the fact that I've been consistently writing on here for six years now, I've said very little about myself. The writing has been…
Show full content

Despite the fact that I've been consistently writing on here for six years now, I've said very little about myself. The writing has been mostly dry, and very technical.

A lot of that has to do with the way the site is laid out - for example, after writing a several thousand word diatribe about Redux or the event loop, it always seemed strange to follow it up the next day with some casual little blog post about what's going on in my life, or something serious or personal. I also wanted people to see the posts I've worked hard on and didn't want them to get drowned out by unrelated posts. I've tried to use tags and categories in the past to solve this, but it never has for me.

So I've decided on having a completely separate area of the site where I can feel free to write about whatever I want. Anything personal, casual, or random will go in "Notes", and I'll keep all my technical writing in "Articles". (One more step towards turning this blog into a digital garden.)

The other reason is, of course, not really knowing how open and personal I want to be on the internet. I don't know how much I want to reveal about myself to every other human being on the planet (and, potentially, in low Earth orbit), especially knowing that everything online is forever. It's been my modus operandi to mostly keep to myself in public spaces on the net for many years. The one area I occassionally interact publicly, Twitter, has been mostly as detached and technical as this site.

I don't have any thoughts or feelings that I'm particularly concerned about sharing, it's just the antithesis between a desire for privacy and a desire to be heard...a desire not to draw too much attention to myself, and a desire to show off.

In any case, this will be a little experiment for fun. Another thing about technical writing is that it's relatively ephemeral, and a lot of my writing has been about learning a particular technology that may not be relevant or helpful in a few years, so it would be nice to have something up that might be relatable for a longer period, and I feel like it would be nice for myself to have some reference of what life was like at various times. I'm going to leave comments off though, if you have any comments you can always just email me.

Right now, in life, I feel very content. I've recently started getting into a habit of waking up early, something that has never been a habit for my entire life, and taking a walk in the brisk cold to get a coffee from one of the many cafes that are a mile or so away. Being a remote worker for the first time has afforded me a lot more free time due to a reduction in commute time, meaning I get more sleep and the mornings aren't a struggle anymore. It's been really nice. I've also been going to the gym at least twice a week.

I enjoy my job. Considering at least half of your waking hours are spent working, I consider this a big deal. There are enough challenges and opportunities to keep it interesting, but not so many challenges that effort seems futile. And the people are great. I've certainly had my fair share of jobs that I dreaded going to in the morning - practically a whole decade of it in my career as a chef - so it really makes a difference.

So yeah, it's a good time for me, overall I feel really content. A stable job, a career I love, all my basic needs met, enough social activity to not feel lonely, enough free time to feel relaxed, no nagging anxiety or blues keeping me down.

I also feel aimless. I don't want much. I think things like maybe eventually I should buy a house instead of renting, but I don't feel much passion for it. I have vague ideas of some bucket list things I'd like to do, like going on a long thru-hike such as the Pacific Crest Trail, or doing a long bike/camp trip. I'd like to be a little healthier and fitter. But I have no additional career, business, or startup aspirations, except maybe a vague notion I'd like to work somewhere like Netflix at some point. I don't have a hobby outside of coding and writing that I feel passionate about at the moment, like art or music. I'd like to be in a good relationship, but I'm feeling pretty happy with my own company right now.

So there's the double edged sword of feeling content. It feels like I can be capable of so much more, but I don't know what. I hoard my time like a dragon hoards gold, turning down requests that come in until they stop, because I don't want to put my time into things that aren't meaningful for me. I'm glad that even when I'm not sure about much, I can still get into the flow state with coding. I would just like to find it again for something that isn't code.

Here's an ABBA song for the occasion. It basically sums it up.

https://taniarascia.com/behind-the-tutorials/
How to Sort, Filter, and Paginate a Table with JavaScript
One thing I've had to do at every job I've had is implement a table on the front end of an application that has sorting, filtering, and…
Show full content

One thing I've had to do at every job I've had is implement a table on the front end of an application that has sorting, filtering, and pagination.

Sometimes, all that will be implemented on the back end, and I've previously documented how to structure those APIs in REST API: Sorting, Filtering, and Pagination. Other times, the data coming back is guaranteed to be small enough that implementing it all in the back end isn't necessary, but still a good idea on the front end to reduce the amount of DOM nodes rendered to the page at a time.

Initially, I would look up libraries like react-table or Ant Design table and try to ensure they had everything I needed. And that's certainly a viable option, but often the libraries don't match the design and needs of your particular case, and have a lot of features you don't need. Sometimes it's a better option to implement it yourself to have complete flexibility over functionality and design.

So I'm going to demonstrate how to do it using React (but conceptually it can apply to any framework or non-framework).

sfp5

Prerequisites
  • Knowledge of JavaScript, React
Goals

Make a table in React that implements:

  • Pagination
  • Sorting for strings, Booleans, numbers, and dates (case-insensitive)
  • Filtering for strings, Booleans, numbers, and dates (case-insensitive)

We're also not going to implement any styles or use any frameworks to reduce complexity.

And here's a CodeSandbox demo: Click me! I'm the demo!

Getting Started

I'm going to set up some data that includes a string, a number, a Boolean, and a date in the dataset, and enough rows that pagination can be implemented and tested. I'll stick some null data in there as well.

const rows = [
  { id: 1, name: 'Liz Lemon', age: 36, is_manager: true, start_date: '02-28-1999' },
  { id: 2, name: 'Jack Donaghy', age: 40, is_manager: true, start_date: '03-05-1997' },
  { id: 3, name: 'Tracy Morgan', age: 39, is_manager: false, start_date: '07-12-2002' },
  { id: 4, name: 'Jenna Maroney', age: 40, is_manager: false, start_date: '02-28-1999' },
  { id: 5, name: 'Kenneth Parcell', age: Infinity, is_manager: false, start_date: '01-01-1970' },
  { id: 6, name: 'Pete Hornberger', age: null, is_manager: true, start_date: '04-01-2000' },
  { id: 7, name: 'Frank Rossitano', age: 36, is_manager: false, start_date: null },
  { id: 8, name: null, age: null, is_manager: null, start_date: null },
]

We'll also want to define the columns on the table.

const columns = [
  { accessor: 'name', label: 'Name' },
  { accessor: 'age', label: 'Age' },
  { accessor: 'is_manager', label: 'Manager', format: (value) => (value ? '✔️' : '✖️') },
  { accessor: 'start_date', label: 'Start Date' },
]

So now can begin making a table abstraction that loops through the columns for the headers, and accesses the proper data for each row. I also added an optional format option if you want to display the data differently. It would be a good idea to use it on the date field.

I prefer to always use brackets and the return statement when mapping in React. It makes debugging and editing a lot easier than with implicit returns.

Table.js
const Table = ({ columns, rows }) => {
  return (
    <table>
      <thead>
        <tr>
          {columns.map((column) => {
            return <th key={column.accessor}>{column.label}</th>
          })}
        </tr>
      </thead>
      <tbody>
        {rows.map((row) => {
          return (
            <tr key={row.id}>
              {columns.map((column) => {
                if (column.format) {
                  return <td key={column.accessor}>{column.format(row[column.accessor])}</td>
                }
                return <td key={column.accessor}>{row[column.accessor]}</td>
              })}
            </tr>
          )
        })}
      </tbody>
    </table>
  )
}

Note: Including the proper keys in this table is essential. If the keys are not the correct unique values, the table will go crazy.

And I'll pass data into my abstract table component.

<Table rows={rows} columns={columns} />

sfp2

Now there's a basic table set up, and we can move on to pagination.

Pagination

There are a lot of ways to set up pagination on the front end.

For example, you have Google, which will show you a next button, a previous button if you're past page 1, and a few additional responses to the left and right of the page currently selected.

sfp1

Personally, I prefer to have "first ️️️⏮️", "previous ⬅️", "next ➡️", and "last ⏭️" options, so that's the way I'll set it up here. You should be able to click "first" or "last" to go to the beginning and end, and "previous" and "next" to go back and forth by a single page. If you can't go back or forward anymore, the options should not appear, or should be disabled.

A lot of libraries seem to handle pagination differently, with page 1 being either 1 or 0. Keep it simple. Just use 1 for page 1. No need for extra calculations.

In the table, I want to calculate:

  • The active page, which is what you'll be updating as you paginate, so it'll go in state
  • The count, which is the total number of rows in a front end only table pre-filtering
  • The rows per page, which I'm setting to a low number so I can test it all with a small data set, but you can also hold this in state if the user should be able to change it
  • The total pages, which will be the total rows divided by rows per page, rounded up
Table.js
const Table = ({ columns, rows }) => {
  const [activePage, setActivePage] = useState(1)  const rowsPerPage = 3  const count = rows.length  const totalPages = Math.ceil(count / rowsPerPage)  const calculatedRows = rows.slice((activePage - 1) * rowsPerPage, activePage * rowsPerPage)
  /* ... */

  return (
    <>
      <table>{/* ... */}</table>
      <Pagination        activePage={activePage}        count={count}        rowsPerPage={rowsPerPage}        totalPages={totalPages}        setActivePage={setActivePage}      />    </>
  )
}

Finally, the calculated rows are the rows that will be displayed in the front end, which ultimately will be affected by filtering, sorting, and paginating. This is the one spot where the page number vs. index needs to be calculate. slice takes a start and end index, so for example on page 3, it would be slice(4, 6), showing the 5th and 6th item in the array.

It would be a good idea to memoize the calculated rows.

I'll start getting the pagination set up.

Pagination.js
const Pagination = ({ activePage, count, rowsPerPage, totalPages, setActivePage }) => {
  return (
    <div className="pagination">
      <button>⏮️ First</button>
      <button>⬅️ Previous</button>
      <button>Next ➡️</button>
      <button>Last ⏭️</button>
    </div>
  )
}

sfp3

Now we just have to do a few calculations. If you're on the first page, there's no "first" or "previous" options, and if you're on the last page, there's no "next" or "last" options.

First and last will always take you to 1 or totalPages, and previous and next just need to add or remove a page.

Meanwhile, you can show the beginning and the end of the rows displayed (such as "showing rows 20-29")

Pagination.js
const Pagination = ({ activePage, count, rowsPerPage, totalPages, setActivePage }) => {
  const beginning = activePage === 1 ? 1 : rowsPerPage * (activePage - 1) + 1
  const end = activePage === totalPages ? count : beginning + rowsPerPage - 1

  return (
    <>
      <div className="pagination">
        <button disabled={activePage === 1} onClick={() => setActivePage(1)}>
          ⏮️ First
        </button>
        <button disabled={activePage === 1} onClick={() => setActivePage(activePage - 1)}>
          ⬅️ Previous
        </button>
        <button disabled={activePage === totalPages} onClick={() => setActivePage(activePage + 1)}>
          Next ➡️
        </button>
        <button disabled={activePage === totalPages} onClick={() => setActivePage(totalPages)}>
          Last ⏭️
        </button>
      </div>
      <p>
        Page {activePage} of {totalPages}
      </p>
      <p>
        Rows: {beginning === end ? end : `${beginning} - ${end}`} of {count}
      </p>
    </>
  )
}

That pretty much covers pagination, it should be easy to modify that for any additional design.

Filtering

Next up is filtering. We want to be able to do case insensitive matching of partial strings and numbers, so enn will match Jenna Maroney and Kenneth Parcell.

I'm going to make a search object, so keys and values can be stored for each value being searched on, and therefore multiple searches can be combined.

Every search should reset the pagination back to page one, because pagination will no longer make sense in the middle when the number of entries have changed.

The count will now be determined by the filteredRows, since there will be less total items after filtering.

Table.js
const Table = ({ columns, rows }) => {
  const [activePage, setActivePage] = useState(1)
  const [filters, setFilters] = useState({})  const rowsPerPage = 3

  const filteredRows = filterRows(rows, filters)  const calculatedRows = filteredRows.slice(    (activePage - 1) * rowsPerPage,    activePage * rowsPerPage  )  const count = filteredRows.length  const totalPages = Math.ceil(count / rowsPerPage)
}

For the filterRows function, we'll just return the original array if no filters are present, otherwise check if it's a string, Boolean, or number, and to the desired check - I have it checking for partial strings, true or false for Booleans, and exact number match (so no 3 for 33...it even allows for searching on Infinity, however pointless that may be).

function filterRows(rows, filters) {
  if (isEmpty(filters)) return rows

  return rows.filter((row) => {
    return Object.keys(filters).every((accessor) => {
      const value = row[accessor]
      const searchValue = filters[accessor]

      if (isString(value)) {
        return toLower(value).includes(toLower(searchValue))
      }

      if (isBoolean(value)) {
        return (searchValue === 'true' && value) || (searchValue === 'false' && !value)
      }

      if (isNumber(value)) {
        return value == searchValue
      }

      return false
    })
  })
}

I made little helper functions for isString, isBoolean, etc - you could use Lodash or whatever else.

Now another row can be added in the headers with a search bar. In a design, this might be populated by clicking on a filter icon, or it can just be displayed in the row as in this example. Although I'm just doing a search bar that requires manual typing for simplicity here, you would probably want to handle each datatype differently - for example, a Boolean could be handled by a dropdown with options for true, false, and clear. You might also have a dataset that includes an enum where the values are known, like role where the options are Writer, Manager, Producer. That could be a dropdown as well instead of making the user type in the values. You could also require only numbers in the number field, and use a date picker for the date field.

Here, if a user types into any search bar, it will add to the list of filters. If a filter is cleared or deleted, it should delete the key. (Leaving the key with a string value of "" can cause problems with the filtering for numbers, Booleans, etc. if you don't handle that case).

The search function:

const handleSearch = (value, accessor) => {
  setActivePage(1)

  if (value) {
    setFilters((prevFilters) => ({
      ...prevFilters,
      [accessor]: value,
    }))
  } else {
    setFilters((prevFilters) => {
      const updatedFilters = { ...prevFilters }
      delete updatedFilters[accessor]

      return updatedFilters
    })
  }
}

In the column header:

<thead>
  <tr>{/* ... */}</tr>
  <tr>
    {columns.map((column) => {
      return (
        <th>
          <input
            key={`${column.accessor}-search`}
            type="search"
            placeholder={`Search ${column.label}`}
            value={filters[column.accessor]}
            onChange={(event) => handleSearch(event.target.value, column.accessor)}
          />
        </th>
      )
    })}
  </tr>
</thead>

sfp4

Now filters are set up, and should handle adding and removing values for all the data types, as well as ignoring null and undefined values.

Sorting

With sorting, we want to be able to do three things for each column:

  • Sort ascending (⬆️)
  • Sort descending (⬇️)
  • Reset sort/no sort (↕️)

Here's a little table, because I often forget how ascending and descending apply to different types of data.

Ascending vs. Descending Type Order Example Description Alphabetical Ascending A - Z First to last Alphabetical Descending Z - A Last to first Numerical Ascending 1 - 9 Lowest to highest Numerical Descending 9 - 1 Highest to lowest Date Ascending 01-01-1970 - Today Oldest to newest Date Descending Today - 01-01-1970 Newest to oldest

Unlike filtering, I'm only going to set up the sort to sort one column at a time. Multi sort might be an option on some tables, but it would be a challenge determining which ones take precedence and how to display it, so I'm going to stick with single sort, which will reset the sort with each new column.

We need to hold two pieces of data in state for sorting:

  • orderBy - which column is being sorted
  • order - whether it's ascending or descending
const Table = ({ columns, rows }) => {
  const [activePage, setActivePage] = useState(1)
  const [filters, setFilters] = useState({})
  const [sort, setSort] = useState({ order: 'asc', orderBy: 'id' })  // ...
}

I'm setting the default accessor to id because I know that's the main key here, but realistically you'd want to pass in a variable into the table to determine the column index.

Once again, we need to check for the types and sort accordingly on the rows - we can do this after filtering them. I'm using the built in localeCompare function, which can handle strings, numbers, and date strings.

function sortRows(rows, sort) {
  return rows.sort((a, b) => {
    const { order, orderBy } = sort

    if (isNil(a[orderBy])) return 1
    if (isNil(b[orderBy])) return -1

    const aLocale = convertType(a[orderBy])
    const bLocale = convertType(b[orderBy])

    if (order === 'asc') {
      return aLocale.localeCompare(bLocale, 'en', { numeric: isNumber(b[orderBy]) })
    } else {
      return bLocale.localeCompare(aLocale, 'en', { numeric: isNumber(a[orderBy]) })
    }
  })
}

The sort function will again restart the pagination, and set the accessor and sort order. If it's already descending, we want to set it to ascending, and so on.

const handleSort = (accessor) => {
  setActivePage(1)
  setSort((prevSort) => ({
    order: prevSort.order === 'asc' && prevSort.orderBy === accessor ? 'desc' : 'asc',
    orderBy: accessor,
  }))
}

In the main table header, I'm adding the sort button next to the column label. Some designs will choose to display the current state of ascending vs. descending, and some will choose to show what it will be after you press the button. I went with showing the current state.

<thead>
  <tr>
    {columns.map((column) => {
      const sortIcon = () => {
        if (column.accessor === sort.orderBy) {
          if (sort.order === 'asc') {
            return '⬆️'
          }
          return '⬇️'
        } else {
          return '️↕️'
        }
      }

      return (
        <th key={column.accessor}>
          <span>{column.label}</span>
          <button onClick={() => handleSort(column.accessor)}>{sortIcon()}</button>
        </th>
      )
    })}
  </tr>
  <tr>{/* ... */}</tr>
</thead>

Now in the table, you'll filter, then sort, then paginate.

export const Table = ({ columns, rows }) => {
  const [activePage, setActivePage] = useState(1)
  const [filters, setFilters] = useState({})
  const [sort, setSort] = useState({ order: 'asc', orderBy: 'id' })
  const rowsPerPage = 3

  const filteredRows = useMemo(() => filterRows(rows, filters), [rows, filters])  const sortedRows = useMemo(() => sortRows(filteredRows, sort), [filteredRows, sort])  const calculatedRows = paginateRows(sortedRows, activePage, rowsPerPage)
  // ...
}

I added in useMemo here to make it more performant and so nobody yells at me. (Would need to add a deeper comparison for pagination to be memoized on a sorted row, though.)

sfp5

And now, the table is ready! You can sort, you can paginate, you can filter, oh my!

Conclusion

Success! We have a table in React implementing sorting, filtering, and pagination without using any libraries. It's ugly as sin but since we know how it all works, we know how to improve it, make it harder, better, faster, stronger.

A few possible improvements:

  • Update the search bar for Boolean types to be a dropdown or checkbox/switch
  • Update the search bar for date types to be a datepicker
  • Implement range search for numbers and dates
  • Implement sorting and filtering for arrays (for example, a list of tags)
  • Find whatever edge my lazy ass didn't test for and fix it

And don't forget to play around with it and try to break it 👇

View the demo! Break me!

Please don't @ me about having to manually type in "true" for Booleans, or manually type in dates instead of using a date picker, or having exact number search instead of date range, etc. That's your homework! Fix it!

And remember, if you're doing all this work on the back end because the data sets are large, you can look at REST API: Sorting, Filtering, and Pagination. Then the code in the table will make API calls instead of handling the arrays and you'll just show a loading state in between each search.

https://taniarascia.com/front-end-tables-sort-filter-paginate/
Redesign: Version 5.0
Over the last week or two I've had a lot of fun building out a new site from scratch. I thought it would be fun to make my site look like a…
Show full content

Over the last week or two I've had a lot of fun building out a new site from scratch. I thought it would be fun to make my site look like a VS Code application. I didn't want to go full gimmick with it, but it's highly inspired by spending all day working in an IDE.

I've always tried to keep my site very clean and minimal and distraction free, but this means that the average person who lands on my site most likely does not explore any further because it doesn't seem like there's anything else to see. So I've decided to change it up this time around and categorize all my posts and make a category sidebar.

v5 dark

v5 light

What do you think? I had fun making little pixel icons, of a floppy disk and a Twitter bird and GitHub OctoCat and so on. I wanted to add a little 8-bit retro vibe to the site and make it more unique than just using emojis or icons. I also implemented a little theme switcher at the top, where not only can you switch between light and dark, but also choose the primary color of the site.

For example, here it is in red:

v5 search

I wrote the CSS from scratch in a single file, because I always find that the easiest and fastest. I've tried the whole CSS-in-JS thing multiple times with multiple libraries, and I still see way more pros to just using plain CSS or SCSS. I did my best to make sure all the focus states look good, and the whole site is tabbable by keyboard.

Implementing the light theme was simply a matter of overwriting some variables in a light theme class.

:root {
  --font-color: white;
}

.light.theme {
  --font-color: black;
}

This website has gone through a lot of iterations. I mean, a lot. Take a look at the very first iteration of the site, from way back in 2014. (Yes, that's an accordion favicon.)

v5 1

The blog has come a long way since then! I wrote about Version 2, which was really semantically somewhere around version 1.863.0, and then I wrote about Version 4, because apparently version 3 didn't happen. Really, I haven't really cared about versioning this site, I just get excited about some change and perpetually tweak it. Which includes making my site look like an MS-DOS prompt as some point.

v5 2

Here's what the site looked like until this release:

v5 3

I'm excited about this design, because the site is more interactive, more personalized, and yet familiar. And I just had fun doing it. Hopefully you like it, too!

https://taniarascia.com/redesign-version-5/
Writing a Sokoban Puzzle Game in JavaScript
So the other day, I made an implementation of a Sokoban puzzle game in JavaScript. Here's the source code and here's the demo. The game…
Show full content

So the other day, I made an implementation of a Sokoban puzzle game in JavaScript.

Here's the source code and here's the demo.

The game consists of a wall, a playable character, blocks, and spots on the ground that are storage locations. The aim of the game is to push all the blocks into all the storage locations. It can be challenging because it's easy to end up in a state where a block can no longer be moved and now you have to restart the game.

Here's the one I made:

The original game has slightly better graphics:

In my version, the big blue dot is the character, the pink dots are the storage locations, and the orange blocks are the crates.

I wrote it up on the fly over the course of a few hours. Making little games is a lot different than what I usually do at work, so I found it to be a fun, achievable challenge. Fortunately with some previous projects (Snek and Chip8) I had some experience with the concept of plotting out coordinates.

Map and entities

The first thing I did was build out the map, which is a two-dimensional array where each row corresponds to a y coordinate and each column corresponds to an x coordinate.

const map = [
  ['y0 x0', 'y0 x1', 'y0 x2', 'y0 x3'],
  ['y1 x0', 'y1 x1', 'y1 x2', 'y1 x3'],
  // ...etc
]

So accessing map[0][0] would be y0 x0 and map[1][3] would be y1 x3.

From there, it's easy to make a map based on an existing Sokoban level where each coordinate is an entity in the game - terrain, player, etc.

Entities
const EMPTY = 'empty'
const WALL = 'wall'
const BLOCK = 'block'
const SUCCESS_BLOCK = 'success_block'
const VOID = 'void'
const PLAYER = 'player'
Map
const map = [
  [EMPTY, EMPTY, WALL, WALL, WALL, WALL, WALL, EMPTY],
  [WALL, WALL, WALL, EMPTY, EMPTY, EMPTY, WALL, EMPTY],
  [WALL, VOID, PLAYER, BLOCK, EMPTY, EMPTY, WALL, EMPTY],
  // ...etc

With that data, I can map each entity to a color and render it to the screen on an HTML5 canvas. So now I have a map that looks right, but it doesn't do anything yet.

Game logic

There aren't too many actions to worry about. The player can move orthogonally - up, down, left, and right - and there are a few things to consider:

  • The PLAYER and BLOCK cannot move through a WALL
  • The PLAYER and BLOCK can move through an EMPTY space or a VOID space (storage location)
  • The player can push a BLOCK
  • A BLOCK becomes a SUCCESS_BLOCK when it's on top of a VOID.

And that's literally it. I also coded one more thing in that's not part of the original game, but it made sense to me:

  • A BLOCK can push all other BLOCK pieces

When the player pushes a block that's next to other blocks, all the blocks will move until it collides with a wall.

In order to do this I just need to know the entities adjacent to the player, and the entities adjacent to a block if a player is pushing a block. If a player is pushing multiple blocks, I'll have to recursively count how many there are.

Moving

Therefore, the first thing we need to do any time a change happens is find the player's current coordinates, and what type of entity is above, below, to the left, and to the right of them.

function findPlayerCoords() {
  const y = map.findIndex(row => row.includes(PLAYER))
  const x = map[y].indexOf(PLAYER)

  return {
    x,
    y,
    above: map[y - 1][x],
    below: map[y + 1][x],
    sideLeft: map[y][x - 1],
    sideRight: map[y][x + 1],
  }
}

Now that you have the player and adjacent coordinates, every action will be a move action. If the player is trying to move through a traversible cell (empty or void), just move the player. If the player is trying to push a block, move the player and block. If the adjacent unit is a wall, do nothing.

function move(playerCoords, direction) {
  if (isTraversible(adjacentCell[direction])) {
    movePlayer(playerCoords, direction)
  }

  if (isBlock(adjacentCell[direction])) {
    movePlayerAndBlocks(playerCoords, direction)
  }
}

Using the initial game state, you can figure out what should be there. As long as I pass the direction to the function, I can set the new coordinates - adding or removing a y will be up and down, adding or removing an x will be left or right.

function movePlayer(playerCoords, direction) {
  // Replace previous spot with initial board state (void or empty)
  map[playerCoords.y][playerCoords.x] = isVoid(levelOneMap[playerCoords.y][playerCoords.x])
    ? VOID
    : EMPTY

  // Move player
  map[getY(playerCoords.y, direction, 1)][getX(playerCoords.x, direction, 1)] = PLAYER
}

If the player is moving a block, I wrote a little recursive function to check how many blocks are in a row, and once it has that count, it will check what the adjacent entity is, move the block if possible, and move the player if the block moved.

function countBlocks(blockCount, y, x, direction, board) {
  if (isBlock(board[y][x])) {
    blockCount++
    return countBlocks(blockCount, getY(y, direction), getX(x, direction), direction, board)
  } else {
    return blockCount
  }
}

const blocksInARow = countBlocks(1, newBlockY, newBlockX, direction, map)

Then, if the block can be moved, it will just either move it or move it and transform it into a success block, if it's over a storage location, followed by moving the player.

map[newBoxY][newBoxX] = isVoid(levelOneMap[newBoxY][newBoxX]) ? SUCCESS_BLOCK : BLOCK
movePlayer(playerCoords, direction)
Rendering

It's easy to keep track of the entire game in a 2D array and render the update game to the screen with each movement. The game tick is incredibly simple - any time a keydown event happens for up, down, left, right (or w, a, s, d for intense gamers) the move() function will be called, which uses the player index and adjacent cell types to determine what the new, updated state of the game should be. After the change, the render() function is called, which just paints the entire board with the updated state.

const sokoban = new Sokoban()
sokoban.render()

// re-render
document.addEventListener('keydown', event => {
  const playerCoords = sokoban.findPlayerCoords()

  switch (event.key) {
    case keys.up:
    case keys.w:
      sokoban.move(playerCoords, directions.up)
      break
    case keys.down:
    case keys.s:
      sokoban.move(playerCoords, directions.down)
      break
    case keys.left:
    case keys.a:
      sokoban.move(playerCoords, directions.left)
      break
    case keys.right:
    case keys.d:
      sokoban.move(playerCoords, directions.right)
      break
    default:
  }

  sokoban.render()
})

The render function just maps through each coordinate and creates a rectangle or circle with the right color.

function render() {
  map.forEach((row, y) => {
    row.forEach((cell, x) => {
      paintCell(context, cell, x, y)
    })
  })
}

Basically all rendering in the HTML canvas made a path for the outline (stroke), and a path for the inside (fill). Since one pixel per coordinate would be a pretty tiny game, I multiplied each value by a multipler, which was 75 pixels in this case.

function paintCell(context, cell, x, y) {
  // Create the fill
  context.beginPath()
  context.rect(x * multiplier + 5, y * multiplier + 5, multiplier - 10, multiplier - 10)
  context.fillStyle = colors[cell].fill
  context.fill()

  // Create the outline
  context.beginPath()
  context.rect(x * multiplier + 5, y * multiplier + 5, multiplier - 10, multiplier - 10)
  context.lineWidth = 10
  context.strokeStyle = colors[cell].stroke
  context.stroke()
}

The render function also checks for a win condition (all storage locations are now success blocks) and shows "A winner is you!" if you win.

Conclusion

This was a fun little game to make. I organized the files like this:

  • Constants for entity data, map data, mapping colors to entities, and key data.
  • Utility functions for checking what type of entity exists at a particular coordinate, and determining what the new coordinates should be for the player.
  • Sokoban class for maintaining game state, logic, and rendering.
  • Script for initializing the instance of the app and handling key events.

I found it easier to code than to solve. 😆

Hope you enjoyed reading about this and feel inspired to make your own little games and projects.

https://taniarascia.com/sokoban-game/
How to Structure and Organize a React Application
There is no consensus on the right way to organize a React application. React gives you a lot of freedom, but with that freedom comes the…
Show full content

There is no consensus on the right way to organize a React application. React gives you a lot of freedom, but with that freedom comes the responsibility of deciding on your own architecture. Often the case is that whoever sets up the application in the beginning throws almost everything in a components folder, or maybe components and containers if they used Redux, but I propose there's a better way. I like to be deliberate about how I organize my applications so they're easy to use, understand, and extend.

I'm going to show you what I consider to be an intuitive and scalable system for large-scale production React applications. The main concept I think is important is to make the architecture focused on feature as opposed to type, organizing only shared components on a global level and modularized all the other related entities together in the localized view.

Tech Assumptions

Since this article will be opinionated, I'll make some assumptions about what technology the project will be using:

I don't have a very strong opinion about the styling, whether Styled Components or CSS modules or a custom Sass setup is ideal, but I think Styled Components is probably one of the best options for keeping your styles modular.

I'm also going to assume the tests are alongside the code, as opposed to in a top-level tests folder. I can go either way with this one, but in order for an example to work, and in the real world, decisions need to be made.

Everything here can still apply if you're using vanilla Redux instead of Redux Toolkit. I would recommend setting up your Redux as feature slices either way.

I'm also ambivalent about Storybook, but I'll include what it would look like with those files if you choose to use it in your project.

For the sake of the example, I'll use a "Library App" example, that has a page for listing books, a page for listing authors, and has an authentication system.

Directory Structure

The top level directory structure will be as follows:

  • assets - global static assets such as images, svgs, company logo, etc.
  • components - global shared/reusable components, such as layout (wrappers, navigation), form components, buttons
  • services - JavaScript modules
  • store - Global Redux store
  • utils - Utilities, helpers, constants, and the like
  • views - Can also be called "pages", the majority of the app would be contained here

I like keeping familiar conventions wherever possible, so src contains everything, index.js is the entry point, and App.js sets up the auth and routing.

.
└── /src
    ├── /assets
    ├── /components
    ├── /services
    ├── /store
    ├── /utils
    ├── /views
    ├── index.js
    └── App.js

I can see some additional folders you might have, such as types if it's a TypeScript project, middleware if necessary, maybe context for Context, etc.

Aliases

I would set up the system to use aliases, so anything within the components folder could be imported as @components, assets as @assets, etc. If you have a custom Webpack, this is done through the resolve configuration.

module.exports = {
  resolve: {
    extensions: ['js', 'ts'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@assets': path.resolve(__dirname, 'src/assets'),
      '@components': path.resolve(__dirname, 'src/components'),
      // ...etc
    },
  },
}

It just makes it a lot easier to import from anywhere within the project and move files around without changing imports, and you never end up with something like ../../../../../components/.

Components

Within the components folder, I would group by type - forms, tables, buttons, layout, etc. The specifics will vary by your specific app.

In this example, I'm assuming you're either creating your own form system, or creating your own bindings to an existing form system (for example, combining Formik and Material UI). In this case, you'd create a folder for each component (TextField, Select, Radio, Dropdown, etc.), and inside would be a file for the component itself, the styles, the tests, and the Storybook if it's being used.

  • Component.js - The actual React component
  • Component.styles.js - The Styled Components file for the component
  • Component.test.js - The tests
  • Component.stories.js - The Storybook file

To me, this makes a lot more sense than having one folder that contains the files for ALL components, one folder that contains all the tests, and one folder that contains all the Storybook files, etc. Everything related is grouped together and easy to find.

.
└── /src
    └── /components
        ├── /forms
        │   ├── /TextField
        │   │   ├── TextField.js
        │   │   ├── TextField.styles.js
        │   │   ├── TextField.test.js
        │   │   └── TextField.stories.js
        │   ├── /Select
        │   │   ├── Select.js
        │   │   ├── Select.styles.js
        │   │   ├── Select.test.js
        │   │   └── Select.stories.js
        │   └── index.js
        ├── /routing
        │   └── /PrivateRoute
        │       ├── /PrivateRoute.js
        │       └── /PrivateRoute.test.js
        └── /layout
            └── /navigation
                └── /NavBar
                    ├── NavBar.js
                    ├── NavBar.styles.js
                    ├── NavBar.test.js
                    └── NavBar.stories.js

You'll notice there's an index.js file in the components/forms directory. It is often rightfully suggested to avoid using index.js files as they're not explicit, but in this case it makes sense - it will end up being an index of all the forms and look something like this:

src/components/forms/index.js
import { TextField } from './TextField/TextField'
import { Select } from './Select/Select'
import { Radio } from './Radio/Radio'

export { TextField, Select, Radio }

Then when you need to use one or more of the components, you can easily import them all at once.

import { TextField, Select, Radio } from '@components/forms'

I would recommend this approach more than making an index.js inside of every folder within forms, so now you just have one index.js that actually indexes the entire directory, as opposed to ten index.js files just to make imports easier for each individual file.

Services

The services directory is less essential than components, but if you're making a plain JavaScript module that the rest of the application is using, it can be handy. A common contrived example is a LocalStorage module, which might look like this:

.
└── /src
    └── /services
        ├── /LocalStorage
        │   ├── LocalStorage.service.js
        │   └── LocalStorage.test.js
        └── index.js

An example of the service:

src/services/LocalStorage/LocalStorage.service.js
export const LocalStorage = {
  get(key) {},
  set(key, value) {},
  remove(key) {},
  clear() {},
}
import { LocalStorage } from '@services'

LocalStorage.get('foo')
Store

The global data store will be contained in the store directory - in this case, Redux. Each feature will have a folder, which will contain the Redux Toolkit slice, as well as actions and tests. This setup can also be used with regular Redux, you would just create a .reducers.js file and .actions.js file instead of a slice. If you're using sagas, it could be .saga.js instead of .actions.js for Redux Thunk actions.

.
└── /src
    ├── /store
    │   ├── /authentication
    │   │   ├── /authentication.slice.js
    │   │   ├── /authentication.actions.js
    │   │   └── /authentication.test.js
    │   ├── /authors
    │   │   ├── /authors.slice.js
    │   │   ├── /authors.actions.js
    │   │   └── /authors.test.js
    │   └── /books
    │       ├── /books.slice.js
    │       ├── /books.actions.js
    │       └── /books.test.js
    ├── rootReducer.js
    └── index.js

You can also add something like a ui section of the store to handle modals, toasts, sidebar toggling, and other global UI state, which I find better than having const [isOpen, setIsOpen] = useState(false) all over the place.

In the rootReducer you would import all your slices and combine them with combineReducers, and in index.js you would configure the store.

Utils

Whether or not your project needs a utils folder is up to you, but I think there are usually some global utility functions, like validation and conversion, that could easily be used across multiple sections of the app. If you keep it organized - not just having one helpers.js file that contains thousands of functions - it could be a helpful addition to the organization of your project.

.
└── src
    └── /utils
        ├── /constants
        │   └── countries.constants.js
        └── /helpers
            ├── validation.helpers.js
            ├── currency.helpers.js
            └── array.helpers.js

Again, the utils folder can contain anything you want that you think makes sense to keep on a global level. If you don't prefer the "multi-tier" filenames, you could just call it validation.js, but the way I see it, being explicit does not take anything away from the project, and makes it easier to navigate filenames when searching in your IDE.

Views

Here's where the main part of your app will live: in the views directory. Any page in your app is a "view". In this small example, the views line up pretty well with the Redux store, but it won't necessarily be the case that the store and views are exactly the same, which is why they're separate. Also, books might pull from authors, and so on.

Anything within a view is an item that will likely only be used within that specific view - a BookForm that will only be used at the /books route, and an AuthorBlurb that will only be used on the /authors route. It might include specific forms, modals, buttons, any component that won't be global.

The advantage of keeping everything domain-focused instead of putting all your pages together in components/pages is that it makes it really easy to look at the structure of the application and know how many top level views there are, and know where everything that's only used by that view is. If there are nested routes, you can always add a nested views folder within the main route.

.
└── /src
    └── /views
        ├── /Authors
        │   ├── /AuthorsPage
        │   │   ├── AuthorsPage.js
        │   │   └── AuthorsPage.test.js
        │   └── /AuthorBlurb
        │       ├── /AuthorBlurb.js
        │       └── /AuthorBlurb.test.js
        ├── /Books
        │   ├── /BooksPage
        │   │   ├── BooksPage.js
        │   │   └── BooksPage.test.js
        │   └── /BookForm
        │       ├── /BookForm.js
        │       └── /BookForm.test.js
        └── /Login
            ├── LoginPage
            │   ├── LoginPage.styles.js
            │   ├── LoginPage.js
            │   └── LoginPage.test.js
            └── LoginForm
                ├── LoginForm.js
                └── LoginForm.test.js

Keeping everything within folders might seem annoying if you've never set up your project that way - you can always keep it more flat, or move tests to its own directory that mimics the rest of the app.

Conclusion

This is my proposal for a sytem for React organization that scales well for a large production app, and handles testing and styling as well as keeping everything together in a feature focused way. It's more nested than the traditional structure of everything being in components and containers, but that system is a bit more dated due to Redux being much easier to implement with Hooks, and "smart" containers and "dumb" components no longer being necessary.

It's easy to look at this system and understand everything that is needed for your app and where to go to work on a specific section, or a component that affects the app globally. This system may not make sense for every type of app, but it has worked for me. I'd love to hear any comments about ways this system can be improved, or other systems that have merit.

https://taniarascia.com/react-architecture-directory-structure/
Using OAuth with PKCE Authorization Flow (Proof Key for Code Exchange)
If you've ever created a login page or auth system, you might be familiar with OAuth 2.0, the industry standard protocol for authorization…
Show full content

If you've ever created a login page or auth system, you might be familiar with OAuth 2.0, the industry standard protocol for authorization. It allows an app to access resources hosted on another app securely. Access is granted using different flows, or grants, at the level of a scope.

For example, if I make an application (Client) that allows a user (Resource Owner) to make notes and save them as a repo in their GitHub account (Resource Server), then my application will need to access their GitHub data. It's not secure for the user to directly supply their GitHub username and password to my application and grant full access to the entire account. Instead, using OAuth 2.0, they can go through an authorization flow that will grant limited access to some resources based on a scope, and I will never have access to any other data or their password.

Using OAuth, a flow will ultimately request a token from the Authorization Server, and that token can be used to make all future requests in the agreed upon scope.

Note: OAuth 2.0 is used for authorization, (authZ) which gives users permission to access a resource. OpenID Connect, or OIDC, is often used for authentication, (authN) which verifies the identity of the end user.

Grant Types

The type of application you have will determine the grant type that will apply.

Grant Type Application type Example Client Credentials Machine A server accesses 3rd-party data via cron job Authorization Code Server-side web app A Node or Python server handles the front and back end Authorization Code with PKCE Single-page web app/mobile app A client-side only application that is decoupled from the back end

For machine-to-machine communication, like something that cron job on a server would perform, you would use the Client Credentials grant type, which uses a client id and client secret. This is acceptable because the client id and resource owner are the same, so only one is needed. This is performed using the /token endpoint.

For a server-side web app, like a Python Django app, Ruby on Rails app, PHP Laravel, or Node/Express serving React, the Authorization Code flow is used, which still uses a client id and client secret on the server side, but the user needs to authorize via the third-party first. This is performed using both an /authorize and /token endpoints.

However, for a client-side only web app or a mobile app, the Authorization Code flow is not acceptable because the client secret cannot be exposed, and there's no way to protect it. For this purpose, the Proof Key for Code Exchange (PKCE) version of the authorization code flow is used. In this version, the client creates a secret from scratch and supplies it after the authorization request to retrieve the token.

Since PKCE is a relatively new addition to OAuth, a lot of authentication servers do not support it yet, in which case either a less secure legacy flow like Implicit Grant is used, where the token would return in the callback of the request, but using Implicit Grant flow is discouraged. AWS Cognito is one popular authorization server that supports PKCE.

PKCE Flow

The flow for a PKCE authentication system involves a user, a client-side app, and an authorization server, and will look something like this:

  1. The user arrives at the app's entry page
  2. The app generates a PKCE code challenge and redirects to the authorization server login page via /authorize
  3. The user logs in to the authorization server and is redirected back to the app with the authorization code
  4. The app requests the token from the authorization server using the code verifier/challenge via /token
  5. The authorization server responds with the token, which can be used by the app to access resources on behalf of the user

So all we need to know is what our /authorize and /token endpoints should look like. I'll go through an example of setting up PKCE for a front end web app.

GET /authorize endpoint

The flow begins by making a GET request to the /authorize endpoint. We need to pass some parameters along in the URL, which includes generating a code challenge and code verifier.

Parameter Description response_type code client_id Your client ID redirect_uri Your redirect URI code_challenge Your code challenge code_challenge_method S256 scope Your scope state Your state (optional)

We'll be building the URL and redirecting the user to it, but first we need to make the verifier and challenge.

Verifier

The first step is generating a code verifier, which the PKCE spec defines as:

Verifier - A high-entropy cryptographic random STRING using the unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "*" / "~" from Section 2.3 of [RFC3986], with a minimum length of 43 characters and a maximum length of 128 characters.

I'm using a random string generator that Aaron Parecki of oauth.net wrote:

function generateVerifier() {
  const array = new Uint32Array(28)
  window.crypto.getRandomValues(array)

  return Array.from(array, (item) => `0${item.toString(16)}`.substr(-2)).join(
    ''
  )
}
Challenge

The code challenge performs the following transformation on the code verifier:

Challenge - BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

So the verifier gets passed into the challenge function as an argument and transformed. This is the function that will hash and encode the random verifier string:

async function generateChallenge(verifier) {
  function sha256(plain) {
    const encoder = new TextEncoder()
    const data = encoder.encode(plain)

    return window.crypto.subtle.digest('SHA-256', data)
  }

  function base64URLEncode(string) {
    return btoa(String.fromCharCode.apply(null, new Uint8Array(string)))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+\$/, '')
  }

  const hashed = await sha256(verifier)

  return base64URLEncode(hashed)
}
Build endpoint

Now you can take all the needed parameters, generate the verifier and challenge, set the verifier to local storage, and redirect the user to the authentication server's login page.

async function buildAuthorizeEndpointAndRedirect() {
  const host = 'https://auth-server.example.com/oauth/authorize'
  const clientId = 'abc123'
  const redirectUri = 'https://my-app-host.example.com/callback'
  const scope = 'specific,scopes,for,app'
  const verifier = generateVerifier()
  const challenge = await generateChallenge(verifier)

  // Build endpoint
  const endpoint = `${host}?
    response_type=code&
    client_id=${clientId}&
    scope=${scope}&
    redirect_uri=${redirectUri}&
    code_challenge=${challenge}&
    code_challenge_method=S256`

  // Set verifier to local storage
  localStorage.setItem('verifier', verifier)

  // Redirect to authentication server's login page
  window.location = endpoint
}

At what point you call this function is up to you - it might happen at the click of a button, or automatically if a user is deemed to not be authenticated when they land on the app. In a React app it would probably be in the useEffect().

useEffect(() => {
  buildAuthorizeEndpointAndRedirect()
}, [])

Now the user will be on the authentication server's login page, and after successful login via username and password they'll be redirected to the redirect_uri from step one.

POST /token endpoint

The second step is retrieving the token. This is the part that is usually accomplished server side in a traditional Authorization Code flow, but for PKCE it's also through the front end. When the authorization server redirects back to your callback URI, it will come along with a code in the query string, which you can exchange along with the verifier string for the final token.

The POST request for a token must be made as a x-www-form-urlencoded request.

Header Description Content-Type application/x-www-form-urlencoded Parameter Description grant_type authorization_code client_id Your client ID code_verifier Your code verifier redirect_uri The same redirect URI from step 1 code Code query parameter
async function getToken(verifier) {
  const host = 'https://auth-server.example.com/oauth/token'
  const clientId = 'abc123'
  const redirectUri = `https://my-app-server.example.com/callback`

  // Get code from query params
  const urlParams = new URLSearchParams(window.location.search)
  const code = urlParams.get('code')

  // Build params to send to token endpoint
  const params = `client_id=${clientId}&
    grant_type=${grantType}&
    code_verifier=${verifier}&
    redirect_uri=${redirectUri}&
    code=${code}`

  // Make a POST request
  try {
    const response = await fetch(host, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: params,
    })
    const data = await response.json()

    // Token
    console.log(data)
  } catch (e) {
    console.log(e)
  }
}

Once you obtain the token, you should immediately delete the verifier from localStorage.

const response = await getToken(localStorage.getItem('verifier'))
localStorage.removeItem('verifier')

When it comes to storing the token, if your app is truly front end only, the option is to use localStorage. If the option of having a server is available, you can use a Backend for Frontend (BFF) to handle authentication. I recommend reading A Critical Analysis of Refresh Token Rotation in Single-page Applications.

Conclusion

And there you have it - the two steps to authenticate using PKCE. First, build a URL for /authorize on the authorization server and redirect the user to it, then POST to the /token endpoint on the redirect. PKCE is currently the most secure authentication system that I know of for a front-end only web or mobile app. Hopefully this helps you understand and implement PKCE in your app!

https://taniarascia.com/oauth-pkce-authorization/
How and When to Use Context in React with Hooks
A while ago, I wrote an article about Using Context API in React. However, most of my examples on that page used Class components, , and…
Show full content

A while ago, I wrote an article about Using Context API in React. However, most of my examples on that page used Class components, static contextType, and Consumer, which is a legacy way of dealing with Context and in TYOOL 2021 we want nice, clean, functional components. I needed to use Context for something recently after quite a while, and I wanted a more succinct explanation using only modern syntax. I decided I'd write a little follow up here for a realistic use of Context.

Context allows you to pass data across any number of React components, regardless of nesting.

Redux or Context?

In a very small application, you might be able to get away with just using Context for most of your global data storage needs, but in a large-scale production environment, you're likely using Redux for global state management. Redux still provides improved performance, improved debugging capabilities, architectural consistency, the ability to use middleware, and more. Therefore, Context is not a replacement for a proper global state management system.

Often, examples for Context will show something like a dark mode toggle, which is fine for a quick example. However, a real-life example of dark theme usage outside of a small blog or website would probably involve a user with settings they can save and persist across any session, not just temporary state in localStorage that gets toggled via Context. In that case, your dark mode state would be saved into Redux, since it would probably be saved as the whole currently logged-in user object, and require an API call to make changes.

So I'm going to provide a summary of just how to set up Context with modern React syntax, then go into an example of using Context and how it might work.

Summary

If you just want some code to copy to create, provide, and consume context, here it is:

You'll usually have one file that uses createContext and exports a Provider wrapper:

Creating
import React, { createContext } from 'react'

export const Context = createContext()

export const Provider = ({ children }) => {
  const [state, setState] = useState({})

  const value = {
    state,
    setState,
  }

  return <Context.Provider value={value}>{children}</Context.Provider>
}

Then you'll wrap whatever component needs access to the Context state with the Provider:

Providing
import React from 'react'

import { Provider } from './Context'
import { ConsumingComponent } from './ConsumingComponent'

export const Page = () => {
  return (
    <div>
      <Provider>
        <ConsumingComponent />
      </Provider>
    </div>
  )
}

And the consuming component can now use the useContext hook to access the data:

Consuming
import React, { useContext } from 'react'

import { Context } from './Context'

export const ConsumingComponent = () => {
  const { state } = useContext(Context)

  return null
}
Example

So when should you use Context, if it's not used for the same purposes as Redux? Well, in my experience, Context makes sense for something a little bit more localized and reusable. For example, you have a Dashboard widget that has controls that are common across many types of widgets. Let's say every widget receives data but can change the view between bar graph, line graph, or table view. In that case, you can create a Context Provider that sets the state of the controls and updates them, and pass them to any consumer.

You use createContext() to create a Context, which also creates a Provider and a Consumer, but you only need the Provider, which will allow any React element below it in the tree to use the Context.

Creating Context DashboardWidget.context.js
import React, { useState, createContext } from 'react'

export const DashboardWidgetContext = createContext()

export const DashboardWidgetProvider = ({ children }) => {
  const [dataView, setDataView] = useState('table')

  const handleChangeView = (value) => {
    setDataViewView(value)
  }

  const value = {
    dataView,
    handleChangeView,
  }

  return <DashboardWidgetContext.Provider value={value}>{children}</DashboardWidgetContext.Provider>
}
Consuming Context

Then you might have a component that handles the actions. This is a contrived example, but it would contain a select that lets you switch between a bar graph, line chart, or table view. Maybe it also has an "export as CSV" button, or some other actions that can apply to all the data in the widget. Now you don't have to handle the controls for each widget individually, but one time for all widgets.

Here you can see the useContext hook allows you to access the data from Context.

DashboardWidgetControls.js
import React, { useContext } from 'react'

import { DashboardWidgetContext } from './DashboardWidget.context'

export const DashboardWidgetControls = ({ label }) => {
  const { dataView, handleChangeView } = useContext(DashboardWidgetContext)

  return (
    <div>
      <select value={dataView} onChange={handleChangeView}>
        <option value="bar_graph">Bar Graph</option>
        <option value="line_chart">Line Chart</option>
        <option value="table">Table</option>
      </select>
    </div>
  )
}

Whatever unique data you need to do on a localized level, you can do in the individual component while still having access to the outer control data. This part might be handled individually, because it might be a grouped or a stacked bar chart, or a nested table, and maybe there are a lot of tweaks that have to happen on that level.

SomeDataComponent.js
import React, { useContext } from 'react'

import { DashboardWidgetContext } from './DashboardWidget.context'

export const SomeDataComponent = () => {
  const { dataView } = useContext(DashboardWidgetContext)

  switch (dataView) {
    case 'table':
      return <Table />
    case 'line_chart':
      return <LineChart />
    case 'bar_chart':
      return <BarChart />
  }
}
Providing Context

Now wherever you need the widget, you can bring in the Provider and the controls. I'll just put it in to a wrapper component:

import React from 'react'

import { DashboardWidgetProvider } from './DashboardWidget.context'
import { DashboardWidgetControls } from './WidgetControls'

export const DashboardWidget = ({ title, children }) => {
  return (
    <WidgetProvider>
      <section>
        <h2>{title}</h2>
        <WidgetControls />
        {children}
      </section>
    </WidgetProvider>
  )
}
DashboardPage.js
import React from 'react';

import { DashboardWidget } from './DashboardWidget';

export const DashboardPage = () => {
  return (
    <div>
      <h1>Dashboard</h1>

      <DashboardWidget title="Distance of Planets to the Sun">
        <PlanetDistance />
      </DashboardWidgetProvider>

      <DashboardWidget title="Time Dilation and the Speed of Light">
        <SpeedOfLight />
      </DashboardWidget>
    </div>
  );
};

Perhaps in this case the actual data is stored in Redux because it might be used elsewhere aside from just this dashboard component, and only the controls need to be handled on a localized level. This is one example where I can see Context making a lot of sense, because passing that data around manually can start to become unintutive or there would be a lot of repetition to handle the same kind of state. I feel like it would be messy to try to handle something like this in Redux, because if you wanted multiple widgets to all be visible at once you'd need it to look like widgets: { widget1: 'bar', widget2: 'table' } or have a separate store for each individual widget.

Conclusion

I hope that was a relatively clear example of a situation in which you might use Context and the modern syntax with which to use it.

https://taniarascia.com/react-context-api-hooks/
Integration Tests with Jest, Supertest, Knex, and Objection in TypeScript
Recently, I set up unit and integration tests for a Node API in TypeScript, and I couldn't find a lot of resources for setting up and…
Show full content

Recently, I set up unit and integration tests for a Node API in TypeScript, and I couldn't find a lot of resources for setting up and tearing down, database seeding, and hooking everything up in TypeScript, so I'll share the approach I went with.

Prerequisites

This article will help if:

Goals
  • You want to be able to spin up a test database, make real API calls with responses and errors, and tear down the database at the end of the tests.

This is not meant to be a complete tutorial that gives step-by-step instructions for every detail, but will give you the big picture of setting up the TypeScript API with Objection and making a test suite for it.

Installation

This app involves objection, knex, pg, express, and typescript, with jest and supertest for testing.

npm i objection knex pg express
npm i -D typescript jest jest-extended supertest ts-jest ts-node
Setup

Assume you have an API with an endpoint at GET /books/:id that returns a Book object. Your Objection model for the Book would look like this, assuming there's a book table in the database:

book.model.ts
import { Model } from 'objection'

export class Book extends Model {
  id!: string
  name!: string
  author!: string

  static tableName = 'book' // database table name
  static idColumn = 'id' // id column name
}

export type BookShape = ModelObject<Book>

Here's an Express app with a single endpoint. It's essential to export app and NOT run app.listen() here so tests won't start the app and cause issues.

app.ts
import express, { Application, Request, Response, NextFunction } from 'express'
import { Book } from './book.model'

// Export the app
export const app: Application = express()

app.use(express.json())
app.use(express.urlencoded({ extended: true }))

// GET endpoint for the book
app.get(
  '/books/:id',
  async (request: Request, response: Response, next: NextFunction) => {
    try {
      const { id } = request.params

      const book: BookShape = await Book.query().findById(id)

      if (!book) {
        throw new Error('Book not found')
      }

      return response.status(200).send(book)
    } catch (error) {
      return response.status(404).send({ message: error.message })
    }
  }
)

The index.ts is where you would set up your database connection and start the app.

index.ts
import Knex from 'knex'
import { Model } from 'objection'

// Import the app
import { app } from './app'

// Set up the database (assuming Postgres)
const port = 5000
const knex = Knex({
  client: 'pg',
  connection: {
    host: 'localhost',
    database: 'books_database',
    port: 5432,
    password: 'your_password',
    user: 'your_username',
  },
})

// Connect database to Objection
Model.knex(knex)

// Start the app
app.listen(port, () => console.log(`*:${port} - Listening on port ${port}`))

So now you have a complete API for the /books/:id endpoint. This API would start with:

tsc && npm start

Or you could use nodemon to get a dev server going.

Migration

In Knex, you can use a migration to seed the schema/data instead of just using raw SQL. To make a migration, you'd just use the Knex CLI to create a migration file:

knex migrate:make initial-schema

And set up the data - in this case, making book table with a few columns:

db/migrations/initial-chema.js
exports.up = async function (knex) {
  await knex.schema.createTable('book', function (table) {
    table.increments('id').primary().unique()
    table.string('name').notNullable()
    table.string('author').notNullable()
  })
}

exports.down = async function (knex) {
  await knex.schema.dropTable('book')
}

Similar instructions are available for seed.

Test Configuration

Your basic jest.config.js would look something like this:

jest-config.js
module.exports = {
  clearMocks: true,
  moduleFileExtensions: ['ts'],
  roots: ['<rootDir>'],
  testEnvironment: 'node',
  transform: {
    '^.+\\.ts?$': 'ts-jest',
  },
  setupFilesAfterEnv: ['jest-extended'],
  globals: {
    'ts-jest': {
      diagnostics: false,
    },
  },
  globalSetup: '<rootDir>/tests/global-setup.ts',
  globalTeardown: '<rootDir>/tests/global-teardown.ts',
}

Note the globalSetup and globalTeardown properties and their corresponding files. In those files, you can seed and migrate the database, and tear it down when you're done.

Global setup

In the global setup, I made a two step process - first connect without the database to create it, then migrate and seed the database. (Migration instructions are in the Knex documentation.)

tests/global-setup.ts
import Knex from 'knex'

const database = 'test_book_database'

// Create the database
async function createTestDatabase() {
  const knex = Knex({
    client: 'pg',
    connection: {
      /* connection info without database */
    },
  })

  try {
    await knex.raw(`DROP DATABASE IF EXISTS ${database}`)
    await knex.raw(`CREATE DATABASE ${database}`)
  } catch (error) {
    throw new Error(error)
  } finally {
    await knex.destroy()
  }
}

// Seed the database with schema and data
async function seedTestDatabase() {
  const knex = Knex({
    client: 'pg',
    connection: {
      /* connection info with database */
    },
  })

  try {
    await knex.migrate.latest()
    await knex.seed.run()
  } catch (error) {
    throw new Error(error)
  } finally {
    await knex.destroy()
  }
}

Then just export the function that does both.

tests/global-setup.ts
module.exports = async () => {
  try {
    await createTestDatabase()
    await seedTestDatabase()
    console.log('Test database created successfully')
  } catch (error) {
    console.log(error)
    process.exit(1)
  }
}
Global teardown

For teardown, just delete the database.

tests/global-teardown.ts
module.exports = async () => {
  try {
    await knex.raw(`DROP DATABASE IF EXISTS ${database}`)
  } catch (error) {
    console.log(error)
    process.exit(1)
  }
}
Integration Tests

With an integration test, you want to be able to seed some data in the individual test, and be able to test all the successful responses as well as error responses.

In the test setup, you can add any additional seed data to the database that you want, creating a new Knex instance and connecting it to the Objection model.

These tests will utilize Supertest, a popular library for HTTP assertions.

Import supertest, knex, objection, and the app, seed whatever data you need, and begin writing tests.

books.test.ts
import request from 'supertest'
import Knex from 'knex'
import { Model } from 'objection'

import { app } from '../app'

describe('books', () => {
  let knex: any
  let seededBooks

  beforeAll(async () => {
    knex = Knex({
      /* configuration information with test_book_database */
    })
    Model.knex(knex)

    // Seed anything
    seededBooks = await knex('book')
      .insert([{ name: 'A Game of Thrones', author: 'George R. R. Martin' }])
      .returning('*')
  })

  afterAll(() => {
    knex.destroy()
  })

  decribe('GET /books/:id', () => {
    // Tests will go here
  })
})
Successful response test

At this point, all the setup is ready and you can test a successful seed and GET on the endpoint.

tests/books.test.ts
it('should return a book', async () => {
  const id = seededBooks[0].id

  const { body: book } = await request(app).get(`/books/${id}`).expect(200)

  expect(book).toBeObject()
  expect(book.id).toBe(id)
  expect(book.name).toBe('A Game of Thrones')
})
Error response test

It's also important to make sure all expected errors are working properly.

tests/books.test.ts
it('should return 404 error ', async () => {
  const badId = 7500
  const { body: errorResult } = await request(app)
    .get(`/books/${badId}`)
    .expect(404)

  expect(errorResult).toStrictEqual({
    message: 'Book not found',
  })
})
Conclusion

Now once you run npm run test, or jest, in the command line, it will create the test_book_database database, seed it with any migrations you had (to set up the schema and any necessary data), and you can access the database in each integration test.

This ensures the entire process from database seeding to the API controllers are working properly. This type of code will give you full coverage on the models, routes, and handlers within the app.

https://taniarascia.com/integration-testing-with-jest-typescript-objection/
Year in Review: 2020 into 2021
Well, 2020, it's been a slice. I've been pretty AWOL lately on all things internet, and I can't decide if I have a lot to say or if I'd…
Show full content

Well, 2020, it's been a slice. I've been pretty AWOL lately on all things internet, and I can't decide if I have a lot to say or if I'd rather say nothing at all, but I've always made an end of the year post since I started this blog - going into 2017, 2018, 2019, 2020, so I'll keep up the tradition and write something today for 2021.

It's fun for me to look over those posts - I can see a zoomed out view of what I did and what I was focused on throughout each year. I wrote hundreds of articles, recorded the occasional song, made a lot of commits, built a few open-source projects, had a break up or three, made a bit of art, traveled all over Europe, and learned a ton.

This year is different though, right? A lot of things happened that we're all aware of, but one thing that happened was that life slowed down a bit. No more commuting to work for me, no more conferences, a lot more spare time. A lot more time on the internet for everyone.

I've spent so long focusing on being productive, feeling like I have to be productive all the time. I made it a point to never make promises or obligations about my creative output, but I've always tried to put out a consistent stream of quality material - at least a good tutorial once a month for the past five years or so. I felt good and productive if I produced something for the web and learned something new and wrote about it.

But that's five years of working 8 hours every day on code during my day job, and often spending the rest of my evening working on articles for DigitalOcean, for this website, for other publications, making my own open-source projects, talking about code on Twitter, and feeling like I should accept or consider all the speaking engagements and podcast requests and anything else that comes my way, because if I start turning them all down, the momentum I'm creating will disappear. (Fortunately I've usually kept “HELL YEAH!” or “no.” by Derek Sivers in mind when responding to things.)

It started getting to the point where I was dreading code, dreading speaking engagements, and the last thing I wanted to do was scroll through the Twitter or Reddit feed and read little arguments about this or that framework intermixed with political outrage, or maintain dozens of open-source GitHub repositories for tutorials that are slowly getting out of date. Not to mention all the emails and comments that would come in, which could be anything from a nice email from a thoughtful person who would become a new friend, to people emailing me their coding questions as if I'm Google, people attempting to shut down or critique every aspect of an article, and of course plenty of "are you single?" and "you're such a good coder for a girl" type emails, or worse things I'd rather not mention.

Of course, the vast majority is positive, but it's always the negative stuff that lingers and bothers you throughout the day, and regardless of good or bad, it all requires mental energy. Even the good is odd to me, because often people will have an idea on me based what little I've put out into the world, and if you came to know me as the impatient, flawed and complex human being I am, it likely would not align and reality might possibly disappoint you. If anything, I'm more scared and hesitant of praise than critiques.

So, I've been burnt out on all code and community after several years of my life revolving around it completely. I'm far from famous, but with nearly 10,000 followers on GitHub and 15,000 on Twitter, I can't just say anything or put anything out there without lots of eyes on it, and it puts more pressure on me than if nobody was paying attention to anything I did.

Interestingly, when I opt out of all of this and only pay attention to the real world, it completely disappears. None of my friends or family really have any idea of anything I've done online. They know I do some coding stuff, but that's where it ends, and that's reality.

There are amazing people all around the world and the internet still gives us so many opportunities to connect and create and help others and do wonderful things, but there is so much I don't like about the way things are right now: being constantly bombarded by ads and distractions, infinite scroll, algorithmic manipulation, addiction, corporate interests, dopamine hits from increasing follows and shares and influence, impatience and a lack of focus, negativity, fear and paranoia from always hearing about all the worst things that are happening in the world, conspiracies and a lack of critical thinking. I just don't want to be part of this and affected by it as much as I can, I want to reject it all and go against the grain and live a slower, simpler life.

One small thing I can do is encourage a few other people to also go against the grain and read a book instead of scrolling a feed, and focus on a few deeper relationships as opposed to collecting and quantifying strangers. I'm learning to be my own best friend. I deleted all my tweets and stopped scrolling through Twitter, I removed my email from any easy-to-find spot, and I make sure not to log into anything like Reddit. Without being tailored to my interests, you'll find that most of these sites are pretty boring and it's easy to just check something out for a few minutes and move on.

I found the latest three articles on The Raptitude interesting and helpful, so I'll share them with you.

Meanwhile, I've been making the best of this weird time. I'm really glad that I still enjoy my job quite a bit and the puzzle-solving aspect of coding that makes it so fun and challenging, but I've wanted to explore things outside of coding in my spare time that makes more use of my hands. I built a PC from scratch. I learned some basic woodworking skills to make my own desk. I learned how to knit. I painted some Bob Ross paintings. I climbed one of the tallest mountains in the U.S. and slept among the stars above the tree line. I took a solo road trip along the coast from L.A. to Seattle in an oversized Jeep Wrangler. I started a new job. I've just been livin'.

Here's me on top of Mount Langley.

langley

Here's my first finished scarf, that I just made last week.

knitting

Here's the desk and PC I built.

desk

Aside from my thoughts, here are some of the stats I usually keep track of.

I finished TakeNote

TakeNote is my biggest project yet, and I wrote all about it here. It's a web-based note-taking app for developers that looks like an IDE and syncs to GitHub. It uses TypeScript, Node, Express, React, Redux, Codemirror, and several other awesome open-source projects. I ultimately decided not to ship it, but I did finish it, and I'm proud of it.

I wrote 21 articles

I did two project write-ups - TakeNote and Chip8.js, several articles on JavaScript for DigitalOcean's fundamentals series, and covered few big concepts like Redux, Docker, and webpack. Honestly, I got a lot more done than I remembered, which is one of those reasons I like to make these posts. It's easy to forget all that you've done over an entire year.

Newsletter

The newsletter is up to 11,283 subscribers. I don't post very often, but it is really the only way I'm communicating with the world since I'm not active on Twitter or Reddit. I'm glad you all are interested in what I'm creating and I still hope to create interesting things in 2021.

Learning

I didn't learn most of the things I wanted to this year. Data structures and algorithms have been on the back burner for a few years now, and I never seem to be able to focus on it. I think the best article I wrote this year is Understanding the Event Loop, Callbacks, Promises, and Async/Await in JavaScript, which is a deep dive on how JavaScript and the event loop work under the hood. This article was something I wanted to write and understand for a long time, so I'm glad I finally did. I still want to learn computer science fundamentals, so next time I decide to sit down and try to learn something coding related that's probably what I'll do.

So, thank you for reading and I hope this answers your questions if you were wondering why I haven't been around or active much. I appreciate all of your support and I'm looking forward to seeing what 2021 brings.

https://taniarascia.com/2020-into-2021/