TypeError: Cannot read property ‘someProp’ of undefined Uncaught TypeError: Cannot read property ‘someProp’ of undefined This error is reported when we read an attribute from null or undefined. This is especially true when a complex front-end project may be interfacing with a variety of back-end services, some of which are unreliable, and the data returned is not in the agreed format.

Here is a deeply nested image:

let nestedObj = {
  user: {
    name: 'Victor'.favoriteColors: ["black"."white"."grey"].// contact info doesn't appear here
    // contact: {
    // phone: 123,
    // email: "[email protected]"
    // }}}Copy the code

Our nestedObj should have a contact property with the corresponding phone and email, but contact doesn’t exist for a variety of reasons (e.g., unreliable service). If we want to read the email message directly, there is no doubt that we cannot, because contact is undefined. If you are not sure if contact exists, you might write code like this in order to safely read the email message:

const { contact: { email } = {} } = nestedObj

// Or so
const email2 = (nestedObj.contact || {}).email

// Or so it goes
const email3 = nestedObj.contact && nestedObj.contact.email
Copy the code

The above approach is to assign a default value to some attribute that may not exist, or to determine whether the attribute exists, so that we can safely read its attribute. This manual default or judgment approach is fine if the object is not deeply nested, but when the object is deeply nested, this approach will crash. Const res = a.b&&a.b.c&&… .

Read deeply nested objects

Let’s look at how to read deeply nested objects:

const path = (paths, obj) = > {
  return paths.reduce((val, key) = > {
    // Val is null or undefined, we return undefined, otherwise we read "next level" data
    if (val == null) { 
      return undefined
    }
    return val[key]
  }, obj)
}
path(["user"."contact"."email"], nestedObj) // return undefined 👍
Copy the code

Now that we can safely read deeply nested objects using the path function, how do we write or update deeply nested objects? Nestedobj.contact. email = [email protected] because you cannot write any attributes to undefined.

Update deeply nested objects

Let’s look at how to safely update attributes:


// Assoc adds or modifies an attribute on x, returns the modified object/array, and does not change the passed x
const assoc = (prop, val, x) = > {
  if (Number.isInteger(prop) && Array.isArray(x)) {
    const newX = [...x]
    newX[prop] = val
    return newX
  } else {
    return {
      ...x,
      [prop]: val
    }
  }
}

// Add or modify attributes to obj according to the path and val provided, without changing the obj passed
const assocPath = (paths, val, obj) = > {
  // Paths is [], returns val
  if (paths.length === 0) {
    return val
  }

  const firstPath = paths[0]; obj = (obj ! =null)? obj : (Number.isInteger(firstPath) ? [] : {});

  // Exit recursion
  if (paths.length === 1) {
    return assoc(firstPath, val, obj);
  }

  // Use the assoc function above to recursively modify the paths containing properties
  return assoc(
    firstPath,
    assocPath(paths.slice(1), val, obj[firstPath]),
    obj
  );
};

nestedObj = assocPath(["user"."contact"."email"]."[email protected]", nestedObj)
path(["user"."contact"."email"], nestedObj) // [email protected]
Copy the code

The assoc and assocPath we write here are pure functions and will not directly modify the data passed in. I wrote a library called JS-Lens, which relies on the above functions and adds some functional features, such as compose. The implementation of this library refers to the code of OCAML-Lens and Ramda departments. Here’s a look at LENS:

const { lensPath, lensCompose, view, set, over } = require('js-lens')

const contactLens = lensPath(['user'.'contact'])
const colorLens = lensPath(['user'.'favoriteColors'])
const emailLens = lensPath(['email'])


const contactEmailLens = lensCompose(contactLens, emailLens)
const thirdColoLens = lensCompose(colorLens, lensPath([2]))

view(contactEmailLens, nestedObj) // undefined
nestedObj = set(contactEmailLens, "[email protected]", nestedObj)
view(contactEmailLens, nestedObj) // "[email protected]"

view(thirdColoLens, nestedObj) // "grey"

nestedObj = over(thirdColoLens, color => "dark " + color, nestedObj)
view(thirdColoLens, nestedObj) // "dark grey"

Copy the code

LensPath receives the Paths array and returns a getter and a setter function. The View uses the getter to read the corresponding property. Set uses the setter function returned to update the corresponding property. Over, like set, updates a property, except that its second argument is a function whose return value is used to update the corresponding property. LensCompose can add lens compose to the incoming lens compose and return a getter and setter function, which will be useful when our data becomes more complex and deeply nested.

Handle nested forms

Let’s take a look at an example of how easy it is to work with nested forms using Lens. The full code for this example is here.

import React, { useState } from 'react'
import { lensPath, lensCompose, view, set } from 'js-lens'

const contactLens = lensPath(['user'.'contact'])
const nameLens = lensPath(['user'.'name'])
const emailLens = lensPath(['email'])
const addressLens = lensPath(['addressLens'])
const contactAddressLens = lensCompose(contactLens, addressLens)
const contactEmailLens = lensCompose(contactLens, emailLens)

const NestedForm = (a)= > {
  const [data, setData] = useState({})
  const value = (lens, defaultValue = ' ') = > view(lens, data) || defaultValue
  const update = (lens, v) = > setData(prev= > set(lens, v, prev))
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        console.log(data)
      }}
    >
      {JSON.stringify(data)}
      <br />
      <input
        type="text"
        placeholder="name"
        value={value(nameLens)}
        onChange={e => update(nameLens, e.target.value)}
      />
      <input
        type="text"
        placeholder="email"
        value={value(contactEmailLens)}
        onChange={e => update(contactEmailLens, e.target.value)}
      />
      <input
        type="text"
        placeholder="address"
        value={value(contactAddressLens)}
        onChange={e => update(contactAddressLens, e.target.value)}
      />
      <br />
      <button type="submit">submit</button>
    </form>
  )
}

export default NestedForm
Copy the code

Finally, I hope this article can be helpful to you, and welcome 👏 to pay attention to my column: a long road ahead.