Constructing a reusable lookup list in React
posted 2021.09.28 by Clark Wilkins, Simplexable

This post is about making a reusable list of key:value pairs that map a user ID to a name (+ optional company name). It covers several points that I had to work out independently of one another, so this might be useful in your own work. The goal was to be able to get full user information for a unique ID one time (aysnchronous call to a DynamoDB table) and make a list so that we could replace {thisUserId} with {thisUserName} anywhere it appears in any child component on the page rendered by React.

problem 1: asynchronicity and React event timing

Apparently, judging from the prodigious amount of material online, a lot of us struggle with the concepts of Asynchronous Javascript. I knew that my execution flow had to be something like this:

  1. Get the entire record set for the object record that will be displayed.
  2. Build a list of unique {userIds} whether they appear in history (having made a change), comments (having written one), or photos (having uploaded one).
  3. Get the actual user's name and (optional) company for the {userId}.
  4. Create a new key/value pair mapping {userId} => {fullUserName + companyName}.
  5. Pass {this.props.actualUserName} down to Child components where the username was already in place and ready for display (no downstream lookup needed).

The first thing I had to realize was this: you cannot wait until you are rendering the page, and then try to build out this list. It's going to be an asynchronous call to get the names, and the page is not waiting you to get that information back. The work has to be done inside doSubmit() once we get the complete data object, and the list has to set in {this.state} which will trigger a new rendering of the page, where you can push this information down to the child-components (comments, photos, and history), and display it.

To get the unique list of {userIds}, I iterated the comment, photos, and history objects like this:

// define an empty array we are going to populate

var userInfo = []; // this is set as an array so we can push to it

// if there are any comments, push the owner ID for each comment into userInfo

if ( comments ) {

Object.entries( comments ).map( function ( commentObject ) {
userInfo.push(commentObject[1].author);
} );

}

// if there are any photos, push the owner ID for each photo into userInfo

if ( photos ) {

Object.entries( photos ).map( function ( photoRecord ) {
userInfo.push(photoRecord[1].author);
} );

}
// there will always be at least one history record (creation),
// so push the event creators into userInfo


Object.entries( pnHistory ).map( function ( value ) {
userInfo.push(value[1]);
} );

// use ES6 Set() constructor to compress this array into
// a set of unique user IDs

userInfo = [...new Set(userInfo)];

This completes steps 1 and 2.

problem 2: remapping a complex object to a simple {id:name} array

This require a couple of nuances.

const userNames = await Promise.all(
userInfo.map ( async ( userId ) => {
let { company, name } = await getThisUser ( userId );
return { userId, company, name };
}
));
  • In the first line, we make userNames wait until all promises are resolved (all async query actions are finished, and we have the user information).
  • In the second line, we make the callback function run asynchronously, so we wait until we get the user information back.
  • The third line is actually getting the user's company and name for each unique user id in {userInfo} from getThisUser
  • The last line is returning an object that has three parameters: the user ID, the user's company, and the user's name.

{userNames} is a zero-indexed array, but we will deal with that at render time (next). This completes step 3.

Having created this object, we now push it to trigger a new rendering of the page.

this.setState({ partNumberInfo, userNames });

Down in the rendering section, we extract the state objects needed to create the results page:

const { data, manufacturers, partNumberInfo, userNames } = this.state;

Note that we are getting {userNames} as created above, so now we can do the mapping for step 4. This is part of a conditional block that waits for the presence of {partNumberInfo} which is only going to be created on doSubmit: the submission of a part number and manufacturer for search.

var namesList = {};

for ( var thisArray of userNames ) {

var { company, name: userName, userId } = thisArray;

if ( company ) { userName += ', ' + company; }

namesList[userId] = userName;

}

Running this down:

  • Initialize an empty object {namesList}.
  • Use a for loop to get each value (the object created for a unique id in step 3).
  • Destructure the object into three variables. (Note: {name}, which is defined externally in the Dynamo DB table, has to be renamed, because this is a reserved keyword in React.)
  • Combine {name} and {companyName}.
  • Store this in {namesList} where the key is the {userId} and the value is the complete user name.

Now, we have completed step 4!

problem 3: passing the remapped information to the controlled (child) component

We can now pass the actual user information down to the child component. In this example, we are showing all photos found in the original part number record. (Note: {key} is zero-indexed, but unique internal to the map, so we can use it to satisfy React's requirement for a unique key for each element.

{ photos && Object.entries ( photos ).sort().map( ( attributes, key ) =>

(
<div key = {key}>
<Photo
description = {attributes[1].description}
key = {key}
owner={namesList[attributes[1].owner]}
URI={attributes[1].URI}
/>
</div>
)

) }

Running this down:

  • The {photos} object is sorted by timestamp (this was set in the original table) and then mapped into the zero-indexed {key} and the photo object {attributes}
  • A new DIV is created and "keyed" with the zero-indexed key from the map.
  • A controlled Photo component is initiated and passed {props}
  • {description} is taken directly from the photo values in {attributes[1]}.
  • {key} is again used to satisfy React.
  • {owner} is taken directly from the photo values in {attributes[1]}, but here, it's remapped to the actual name/company using {namesList}. This is the unique names lookup in action.
  • {URI} is taken directly from the photo values in {attributes[1]}.

So we have now accomplished the goal of a reusable names list, defined once and rendered everywhere it's needed.