UI Builder Tips #3: Managing Objects in State

Reece PoulsenReece Poulsen
5 min read

UI Builder Tips Series

Welcome to the UI Builder Tips series! I'll use this series to document what I've learned while working with ServiceNow's UI Builder. Since this is a learning process, these blog posts should be seen as a set of notes.

The content of these posts will likely change often to include new information as I continue to learn. Feel free to leave feedback or corrections in the comments, and I'll update the posts as needed. Let's learn together!

Context

For You and I Builder Live's Build Along Month, we were challenged to create a contact entry page where you can input data for a new contact record. We had the option to use the standard form and form controller or to get more creative. I decided to challenge myself and implement a custom form to collect the contact data.

There was a decently long list of fields I needed to collect from the user to create the contact: name, email, phone number, note, job title, company, etc. I didn't want to store each field as a separate client state variable that I would have to manage later. So, I decided to create a single contactData client state variable and set its default value to an empty JSON object. Then, when users input data into each field, the triggered events would update the contactData object in the state with a key corresponding to the field and a value corresponding to the inputted value. Simple enough, right?

I started writing my update client state parameter event handlers for the 'Input value set' event on the input fields like normal. My first approach was to get the contactData out of client state, modify the key/value pair for the field, and then just return the new object to be updated into state. This would be simple enough because the event payload contained the name of the input (the key) and the value entered (value) like so:

function evaluateEvent({api, event}) {
    const contactData = api.state.contactData;
    const key = event.payload.name;
    const value = event.payload.value;

    contactData[key] = value;

    return {
        propName: "contactData",
        value: contactData
    };
}

The Problem

This worked great for a while but then I ran into a problem. I wanted to keep the user from going to the next step of the contact entry if they didn't add a name. So, I setup the next button to be disabled if the name field wasn't filled out.

This simple check !api.state.contactData.name would be true if there wasn't a name value in the state and false if there was. I thought this would work perfectly because true would disable the button and false would enable it. But when I tried, I noticed the button wasn't working as expected. I was entering a name in the input, but the button was still disabled! It seemed like the button wasn't detecting that the name value on the contactData client state object had been updated even though it should have been! The component wasn't re-rendering to enable the button, frustrating!

Pass-by-Reference vs Pass-by-Value

I did some more digging into the issue. I reviewed my code several times. It all seemed so simple, I wasn't sure where the problem was! Then, I remembered a lesson from one of my C++ classes in college about pass-by-reference and pass-by-value. Basically, when a parameter or object is passed to a function, it can either be a copy of that data (pass-by-value) or a reference that points back to the original data (pass-by-reference).

Imagine you have an apple, and your friend wants one too. With pass-by-value, you would buy another identical apple and give it to your friend. With pass-by-reference, both you and your friend would share the same apple.

After some research I found that in JavaScript, primitive types (like numbers, strings, booleans) are passed by value, while objects and arrays are passed by reference.

This meant that my original script was updating api.state.contactData.name directly without using the supported api.setState() method. Updating a state object directly and then trying to set it to the already updated version makes UI Builder think that no change happened, so it doesn't broadcast that the state was updated. That's why my button component wasn't re-rendering!

function evaluateEvent({api, event}) {
    const contactData = api.state.contactData;
    const key = event.payload.name;
    const value = event.payload.value;

    contactData[key] = value; // πŸ‘ˆ This is the problem!

    // The contactData object is already updated because I wrote 
    // directly to it on the line above so when it is returned 
    // UIB doesn't think an update happened πŸ€·β€β™‚οΈ
    return {
        propName: "contactData",
        value: contactData 
    };
}

The Solution

Once I identified the problem, the next step was to find a solution. I needed an easy way to copy the contactData object from the client state into my own variable, modify my copy, and then update the state with my copy. I considered iterating over each key and storing the values in my copy object or using JSON.stringify to convert the object to a string and then JSON.parse to convert it back, but it seemed like there had to be a better way. Enter the ✨JavaScript Spread Operator✨!

The Spread Operator ...

The JavaScript Spread Operator ... is a newer ES6 JavaScript feature that many ServiceNow developers might not be familiar with. It allows you to copy the contents of an array or an object into a new array or object. This is particularly useful because arrays and objects are always pass-by-reference. One of the key uses of the spread operator is to solve problems like this!

After a quick modification to my script, everything started working as expected! I changed the const contactData = api.state.contactData line to const contactData = {...api.state.contactData}. This new version of the code tells JavaScript to take the contents of the original object and spread them out inside my new object. This effectively creates a perfect copy and eliminates the reference problem!

function evaluateEvent({api, event}) {
    // Use the spread operator when pulling an object out of client
    // state, problem solved!πŸ‘‡
    const contactData = {...api.state.contactData}; 
    const key = event.payload.name;
    const value = event.payload.value;

    contactData[key] = value;

    return {
        propName: "contactData",
        value: contactData
    };
}

Conclusion

The JavaScript Spread Operator ... is a handy tool to remember when working with objects stored in client state. Next time you think your client state object should be updated but isn't, remember this blog post!

0
Subscribe to my newsletter

Read articles from Reece Poulsen directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Reece Poulsen
Reece Poulsen

Back in my freshman year of college, I was introduced to the world of programming, and it immediately caught my attention. The idea of creating something new with just a little learning and a couple of lines of code fascinated meβ€”it was like discovering a whole new universe of possibilities! Ever since that moment, I've been on a continuous journey to expand my programming knowledge and skills. Now I'm a relatively new software developer on the ServiceNow platform, and I'm eager to explore its potential. Through this blog, I hope to share some of the things I'm learning along the way. Let's dive into the world of ServiceNow together and uncover the tricks and insights that can make a difference for us as developers!