Dread Saurian

18 Aug 2019

Three months after getting this beautiful model for my birthday, I’m calling it finished. Here’s my Dread Saurian.

Photo Credit: NOVA Open Capital Palette

Dread Saurian - Color Scheme by Amanda Killingsworth

Dread Saurian - Color Scheme by Amanda Killingsworth

Dread Saurian

Dread Saurian

Dread Saurian Dread Saurian
Dread Saurian Dread Saurian

Using Proxy() in Javascript to Detect Changes

01 Aug 2019

I ran into an issue with my side project AoS Reminders recently where I was mutating an object that wasn’t supposed to be mutated.

I was having trouble tracking down the mutation, until I stumbled upon the Proxy object, a helpful tool for any Javascript developer.

const obj = { key1: 'value', key2: 'value2' }

const handler = {
  get(target, key) {
    console.log('Reading value from ' + key)
    return target[key]
  },
}

const proxiedObj = new Proxy(obj, handler)

// When we run this `if` statement, console will log
// 'Reading value from key1'
if (proxiedObj.key1) { }

This can be useful for detecting when your objects are being accessed.

Another great example of using Proxy is for object validation:

// Use the proxy handler as a validator
const validator = {
  set: (obj, prop, value) => {
    // Validate that we passed in a correct `type` value
    if (prop === 'type') {
      const validTypes = ['Mage', 'Barbarian', 'Archer']
      if (value === null) {
        throw new TypeError('Cannot set to type to null');
      }
      if (!validTypes.includes(value)) {
        throw new TypeError('Cannot set to type to ' + value);
      }
    }

    // Store the new value in your object
    obj[prop] = value;

    // `set` expects a boolean response if successful
    return true;
  }
};

let char = new Proxy({age: 40, stars: 5}, validator);

char.name = 'Dennis'      // Other keys are not validated
char.type = 'Golden God'  // Throws `Cannot set to type to Golden God`
console.log(char.type)    // undefined
char.type = 'Mage'        // Passes validation
console.log(char.type)    // 'Mage'
char.type = null;         // Throws `Cannot set to type to null`
console.log(char.type)    // 'Mage'

While useful, you should really be using Typescript and enforcing your object keys that way ;)

Hope this helps someone! Just thought I’d share a small, neat feature of JS I hadn’t used before.


The Case for Using Immer

26 Jul 2019

In this post, I’d like to convince you to use Immer, 2019’s hottest JavaScript framework.

What is Immer?

Immer is a tiny package that allows you to work with immutable state in a more convenient way.

The basic idea is that you will apply all your changes to a temporary draftState, which is a proxy of the currentState. Once all your mutations are completed, Immer will produce the nextState based on the mutations to the draftState. This means that you can interact with your data by simply modifying it while keeping all the benefits of immutable data.

Why Immer?

We write a lot of boilerplate when using Redux and updating our store. Every developer familiar with even remotely complex reducers has seen this pattern before:

const someReducer = (state, action) => {
  const newEntry = {
      ...state.entries[action.payload.id],
      completed: true
  }
  return {
      ...state,
      entries: {
          ...state.entries,
          [action.payload.id]: newEntry
      }
    }
}

All that code just to set one entry to completed: true! This “extra” destructuring work is required because Redux needs us to return a complete copy of the state.

What if we could just modify the entry we want, and Redux could figure out the rest? Well, that may be expecting too much of Redux, but luckily, we can now use Immer!

With Immer, the above code would look like:

import produce from "immer"

const someReducer = produce((state, action) => {
  state.entries[action.payload.id].completed = true
})

From a developer point of view, I think the benefits are obvious. We’re no longer spending time trying to destructure and then restructure state. We’re just performing a simple operation and moving on with our lives.

Generic Examples

To begin with, what does an Immer function even look like?

Here’s an example:

import produce from 'immer'

const firstState = {}

const secondState = produce(firstState, draft => {
  // empty function
})

console.log(secondState === firstState) // true

// Let's begin using Immer by adding a { 'key': 'value' } pair to the state
const thirdState = produce(firstState, draft => {
  draft['key'] = 'value'
})

console.log(firstState) // {}
console.log(thirdState) // { key: 'value' }

// Now we get to see the magic of Immer
const fourthState = produce(thirdState, draft => {
  delete draft['key']
  draft['newKey'] = { one: 'two' }
  draft['rubicon'] = 'awesome'
})

// thirdState is unmodified, of course!
console.log(thirdState) // { key: 'value' }
console.log(fourthState)
// {
//   newKey: { one: 'two' },
//   rubicon: 'awesome'
// }

Okay, let’s do something a little more realistic. Let’s pretend we’re building an app where users can check a certain field. A todo list or grocery shopping app would make sense.

import produce from 'immer'

const state = [
  {
    id: 1,
    checked: false
  },
  {
    id: 2,
    checked: false
  }
]

const result = produce(state, draft => {
  draft[0].checked = true
  draft.push({ id: 3, checked: false })
})

console.log(result)

// [
//   { id: 1, checked: true },
//   { id: 2, checked: false },
//   { id: 3, checked: false }
// ]

Real World Examples

I think you’re probably starting to get the idea. But let’s get into why this would work for advanced applications where you’re using and abusing Redux heavily.

Here is a real example of a very painful piece of code that we had written at work. We have a UX flow that allows the user to change the height of a displayed chart. The only thing this action handler wants to do is update the height property of a specific chart. But unfortunately, we have to fully recreate the state via destructuring - and it gets ugly!

Before Immer
export const setChartHeightActionHandler = (state, { meta }) => {
  const { chartId, height } = meta
  const { byId } = state

  const chartObject = { ...byId[chartId] }
  const { chartDimensions } = chartObject
  const thisChartDimensions = chartDimensions[chartId]

  return {
    ...state,
    byId: {
      ...byId,
      [chartId]: {
        ...chartObject,
        chartDimensions: {
          ...chartDimensions,
          [chartId]: {
            ...thisChartDimensions,
            height // This is literally the only change made
          }
        }
      }
    }
  }
}

Ugh! That is pretty gross. Let’s use Immer instead.

After Immer
import produce from "immer"

export const setChartHeightActionHandler = produce((state, { meta }) => {
  const { chartId, height } = meta
  state.byId[chartId].chartDimensions[chartId].height = height
})

Boom! The exact same functionality as above.

(If this example doesn’t win you over, I have nothing to offer you.)

Guess which one is easier to understand? Guess which one doesn’t take 15 minutes and two developers to write? (True story).

Let’s refactor another action handler - this one deletes a single entry in the store.

Before Immer
export const deleteExperimentActionHandler = (state, { meta }) => {
  const { experimentId, projectId } = meta

  if (!state.byId[experimentId]) return state

  const byId = {
    ...state.byId
  }
  delete byId[experimentId] // The only operation actually needed

  return {
    ...state,
    byProject: {
      [projectId]: Object.values(byId)
    },
    byId
  }
}
After Immer
import produce from "immer"

export const deleteExperimentActionHandler = produce((state, { meta }) => {
  // Returning undefined is the same as saying "keep the same state"
  if (!state.byId[meta.experimentId]) return
  delete state.byId[meta.experimentId]
})

Is that super dope or what? We accomplish the same task with two lines of code.

Caveats

Immer does have a downside - namely, it’s about twice as slow as a well-written reducer. Immer recommends not using this library when operating on tens of thousands of objects - otherwise, the performance difference is neglible.

Summary

Use Immer wherever it makes sense to you. It’s easy to start with, and the developer benefits will become clear immediately. Your functions will also become easier to test and debug, since you’ll be rid of the noise that comes along with re-creating state all the time!

For those of you using TypeScript - the Immer library is strongly typed! Very cool!


React Hooks - Check If A Component Is Mounted

11 Jul 2019

If you’ve ever worked with a multi-page React app, you will invariably run into the dreaded warning in your console:

Warning: Can't perform a React state update on an unmounted component.

The most common cause of this warning is when a user kicks off an asynchronous request, but leaves the page before it completes. I especially wanted to know how to do this with hooks, because hooks are awesome.

I wanted to know how to avoid this classic React warning - here’s my solution.

const SampleComponent = () => {

    const [counter, setCounter] = useState(0)

    // Here's how we'll keep track of our component's mounted state
    const componentIsMounted = useRef(true)
    useEffect(() => {
        return () => {
            componentIsMounted.current = false
        }
    }. []) // Using an empty dependency array ensures this only runs on unmount

    const incrementCounterAsync = useCallback(()=> {
        async () => {
            try {
                await someLongRunningProcess()
                if (componentIsMounted.current) {
                    setCounter(counter + 1)
                }
            } catch (err) {
                // Handle your error
            }
        }
    }, [setCounter])

    render (
        // Display the counter + a button to increment it
    )
}

To explain the above code snippet, imagine we have a sample component that simply displays a counter, and a button to increment the counter.

When we click the button, it fires off someLongRunningProcess() - this could be a backend operation or whatever. Use your imagination.

When someLongRunningProcess() has completed, we make sure we’re still mounted via the componentIsMounted variable! If the component is still mounted, we can modify the state, no problem. If it’s not mounted - hey, that’s alright, we won’t touch the state.

This will dodge that nasty React warning! Have fun!