React For Experience Builder Developer Edition

4141
14
11-07-2023 01:56 PM
JeffreyThompson2
MVP Regular Contributor
22 14 4,141

This post is only for people using Developer Edition, if you are on the Online or Enterprise Edition, go away. Go away and be grateful that you do not have to read on, only madness lies ahead.

Hi coders, if you are reading this, you have decided to use Experience Builder Developer Edition. I commend your bravery, if not necessarily your wisdom. Have you used React before? If not, getting started in Developer Edition is going to be a rough ride. My number one tip for anyone booting up Developer Edition for the first time is to stop and spend several days studying React before ever trying to install Developer Edition. And yes, I do mean days. It takes that long to get enough of a grasp on the basics to begin to be able to actually use it. It is different enough from regular javascript that I think it is fair to classify it as an entirely separate language. React operates very differently than any programing language you have used before with the possible exception of Angular or Vue.js, which are other React-like frameworks. Using React is sort of like dancing, if you and your gorgeous bride React are in sync, it will be a beautiful experience and if not, you will have several broken toes and a visit to a divorce attorney.

I cannot teach you React. It is too big a subject and I am not an expert. Here is the official React guide. Start learning here. But I will share what I think are the most important concepts to keep in mind. Some of the concepts in React get a bit circular, but I will try to organize this post the best I can. Just trust that if I throw out a new term, I will get around to explaining it later. I am not sure if it would be better to read this or the official React guide first. Maybe make it a sandwich. Read this. Then, read the official guide and then read this again when it sounds slightly less insane.

React: A History

We will start with a little history lesson. I swear this is relevant. React was invented by Facebook in 2013 and became open-source in 2014. React was invented to solve a problem Facebook had users would scroll through their newsfeed and when they hit the bottom of the page, they might visit another website or even worse, turn off their computers. React was designed to continuously get or send data to or from an API without ever needing to reload the page, so now scroll all you want, you will never see the bottom of your Facebook feed.

Which brings me to the first Experience Builder relevant fact about React, when within an Experience, the page never reloads. You may have multiple pages within your Experience, but React is kind of lying to you, everything is happening in a single page and just showing you different things. This is generally speaking a good thing as it usually results in a faster, smoother user experience, but it may become an issue when combined with a quirk of Experience Builder. Typically on a React page, if you as a user click an X to close a popup, that popup would be destroyed and a new one created if you reopened the popup. But Experience Builder never destroys widgets, even when switching between pages. This can lead to some unexpected behavior, if you do not anticipate it.

Back to the history lesson.

In the beginning, React allowed for components to be made within classes or functions. Using classes had some minor advantages, so they were the preferred method, until 2018 when React Hooks came out. All of the React methods starting with use, like useState or useEffect, are Hooks and Hooks made React so much better and easier. But, Hooks cannot be used in class based components. So, over the next couple of years function based components became the preferred method to write React. Class based components are still 100% valid and functional and the React team swears they will never be removed, but you should consider them depreciated. Always write function based components, if you can. When looking for learning resources, check that they are 2020 or later and if they start talking about classes, move on to something else. The class based React documentation has been buried in the official React docs, but you can find it here.

I'd like to tell you, you can just ignore class based React completely. I'd like to say that, but... look at the dates in that last paragraph, 2018 to 2020. The exact timeframe that Experience Builder was being developed. Much of Experience Builder is in class based components, including most of the widget coding examples provided by ESRI. You may not need to write in class based React, but you will probably need to be able to read it. Sorry. 😢

JSX and the Virtual DOM

When a user loads a React application, the HTML contains only a single node. Everything that happens on the screen is the result of React manipulating what is known as the Virtual DOM. You should not, in standard React, ever attempt to directly manipulate the actual DOM, using methods like innerHTML or appendChild, etc.. Bad things happen to React developers who mess with the DOM.

JSX is a mashup language of HTML and Javascript. All of the HTML elements and React components are valid JSX elements. Every React component must resolve to a single return statement that returns a single JSX node. Like HTML, JSX nodes can contain any number of elements.

A React fragment (it looks like this, <></>) can be used for a component that should not paint something to the screen directly, but will add items through direct DOM manipulation. Direct DOM manipulation should be avoided at all cost, but when using the ESRI Javascript API there is often no way around doing it. Experience Builder 1.13 will be based on the 4.28 version of the API and includes map components that were specifically designed to help deal with this problem. For versions 1.13 or later of Experience Builder, you should never need to directly manipulate the DOM.

State and Re-renders

Components in React frequently re-render. During a re-render a component will clear its memory and forget everything it knows unless a variable is stored in state or a ref. The three triggers for a re-render are a change to state, a change to props, or a re-render higher up in the React tree.

Here is a widget designed to add and remove a layer from a map. It doesn't work quite right. It will add the layer, but it can't remove it.

 

import { React, AllWidgetProps } from 'jimu-core'
import { JimuMapViewComponent, JimuMapView } from 'jimu-arcgis'
import FeatureLayer from 'esri/layers/FeatureLayer'

const { useState } = React

const Widget = (props: AllWidgetProps<any>) => {
  const [jimuMapView, setJimuMapView] = useState<JimuMapView>()
  const layer = new FeatureLayer({
      url: 'URL1'
    })

  const activeViewChangeHandler = (jmv: JimuMapView) => {
    if (jmv) {
      setJimuMapView(jmv)
    }
  }

  const formSubmit = (evt) => {
    evt.preventDefault()
    jimuMapView.view.map.add(layer)
  }
  
const formSubmit3 = (evt) => {
    evt.preventDefault()

    jimuMapView.view.map.remove(layer)
  }
  
  
  return (
    <div className="widget-starter jimu-widget">
      {
        props.useMapWidgetIds &&
        props.useMapWidgetIds.length === 1 && (
          <JimuMapViewComponent
            useMapWidgetId={props.useMapWidgetIds?.[0]}
            onActiveViewChange={activeViewChangeHandler}
          />
        )
      }

      <form onSubmit={formSubmit}>
        <div>
          <button>Add Layer</button>
        </div>
      </form>
	   <form onSubmit={formSubmit3}>
        <div>
          <button>Delete</button>
        </div>
      </form> 
	  
    </div>
  )
}

export default Widget

 

The fundamental problem is that the widget will forget the reference to layer between re-renders, so will not be able to find the layer later to remove it. Here is this widget updated to use state. It works as expected.

 

import { React, AllWidgetProps } from 'jimu-core'
import { JimuMapViewComponent, JimuMapView } from 'jimu-arcgis'
import FeatureLayer from 'esri/layers/FeatureLayer'

const { useState } = React

const Widget = (props: AllWidgetProps<any>) => {
  const [jimuMapView, setJimuMapView] = useState<JimuMapView>()
  const item = new FeatureLayer({
      url: 'URL1'
    })
  const [layer, setLayer] = useState(item)

  const activeViewChangeHandler = (jmv: JimuMapView) => {
    if (jmv) {
      setJimuMapView(jmv)
    }
  }

  const formSubmit = (evt) => {
    evt.preventDefault()
    jimuMapView.view.map.add(layer)
  }
  
const formSubmit3 = (evt) => {
    evt.preventDefault()

    jimuMapView.view.map.remove(layer)
    setLayer(null)
  }
  
  
  return (
    <div className="widget-starter jimu-widget">
      {
        props.useMapWidgetIds &&
        props.useMapWidgetIds.length === 1 && (
          <JimuMapViewComponent
            useMapWidgetId={props.useMapWidgetIds?.[0]}
            onActiveViewChange={activeViewChangeHandler}
          />
        )
      }

      <form onSubmit={formSubmit}>
        <div>
          <button>Add Layer</button>
        </div>
      </form>
	   <form onSubmit={formSubmit3}>
        <div>
          <button>Delete</button>
        </div>
      </form> 
	  
    </div>
  )
}

export default Widget

 

State is not live data. It is a snapshot of data at the last re-render. This can result in all sorts of confusing errors. To make things more confusing, calling useState() does not instantly update state. React cues all of the useState() calls and runs them in order during the re-render.

 

const [num, setNum] = useState(0)
setNum(num + 1) //num would be 1 on the next render.
setNum(num + 5) //Now, num would be 5 on the next render.
setNum(num - 1) //Now, num will be -1 on the next render.
console.log(num) //Output is 0

const [num, setNum] = useState(0)
setNum(num + 1) //num would be 1 on the next render.
setNum(5) //Now, num would be 5 on the next render.
setNum(num - 1) //Now, num will be 4 on the next render.
console.log(num) //Output is 0

 

Confusing, isn't it? Welcome to the wonderful world of React.

React must know what to render at every microsecond. If at any point React does not know what to render, the component will break and display an error message. The most difficult render to handle is often the very first one as it often occurs before the required data is loaded. This is often accomplished with a ternary statement, like the one below. Ternary statements, deconstructing arrays and various ways of testing for truthiness are rarely used features of Javascript that are incredibly important to writing good React.

 

{value ? <MyComponent></MyComponent> : <Loading></Loading>}

 

If you have ever wondered why so many modern websites show a shimmery, data-free outline of the website design before the actual site loads, this is why. (Also, psychological research finds that users that are shown shiny things are more patient with load times and think websites load faster.) The UI StoryBook includes a Loading component and many other useful UI elements.

UseRef()

Other than state, the other way to store data between re-renders is the useRef() Hook. Changes to a ref occur instantly and do not trigger a re-render. It looks something like this.

 

 

const num = useRef(0)
//Use the .current property to access the value of a ref.
console.log(num.current) //Output is 0
num.current = 5
console.log(num.current) //Output is 5

 

UseEffect

UseEffect is the React Hook I use most often in Experience Builder. With useEffect(), you can create a block of code that runs once when the component is mounted, every time a component re-renders, when a specific value or set of values is changed, or when the component is unmounted. But remember that widgets do not fully unmount in Experience Builder, so that last option will not work in the highest level of an Experience Builder widget. Each of these uses of useEffect() has a different syntax. I keep this page bookmarked because I use it all the time to find the proper useEffect() syntax.

Props and One-Way Data Flow

A React application is structured like a tree or at least the part of a tree that is above ground. The stuff closest to the root is referred to as being "high in the React tree" and we get "lower or deeper in the React tree" as we get into sub-components. (I guess people at Facebook don't get outside very often.) Water flows up the trunk, through the branches and out the leaves. Water never goes back down the tree. If a squirrel, starts jumping up and down on a branch, it will (or should) only affect that branch, not the trunk or a totally separate branch. If the whole tree starts shaking when a squirrel jumps on one branch, run away, that tree is about to fall over.

Another common React metaphor is that of children and parents. With the key phrase being, parents are allowed to alter their children, but children are not allowed to alter their parents. (The people at Facebook sound like pretty lousy parents.) 

It's not often shown in the ESRI coding samples, but your widgets can and often should call in sub-components. If you are using any elements from the UI StoryBook, you are already doing this. Adding additional levels and branches within your widgets is often necessary to get your desired outcome. My custom List widget is five levels deep it branches in the middle and comes back together at the lowest level.  Data in React should always* flow from the root, which is much higher in the React tree than anything you should be doing in Experience Builder to the lowest levels, which are your widgets. (Developer Edition comes with a folder called "your-extensions", if you are doing anything outside of that folder, you are probably doing something wrong.) Trying to fight one-directional flow is a React anti-pattern that will infuriate you and probably result in undesired outcomes.

One-way data flow is managed through props. Props must be explicitly passed from a parent component to the next child down in the React tree. The props the child receives are immutable. If the props are changed by the parent, the component will re-render. Children must receive props by passing them as an argument into the component function. It looks something like this.

 

//Passing props down
render(
 <MyComponent myProp={'value'}></MyComponent>
)

//Recieving props
const MyComponent = (props) => {
  console.log(props.myProp) //Output is 'value'
}

 

*Redux Or Breaking One-Way Data Flow

Experience Builder comes packaged with a state manager called Redux. As you read through the React docs, you will encounter a design pattern called "lifting up state".  In lifting up state, we rewrite a set of components containing a parent and two or more children so that state is managed by the parent and passed to the children as props, effectively allowing the children to communicate with each other. State managers take this concept to the logical extreme. They sit at the top of the React tree thus allowing any component to send data up to the state manager and retrieve it in any other component regardless of what branch it is on. ESRI has kindly done all the heavy lifting in setting up Redux, so all you need to know is how to send and receive messages. Note that the messages are received through props, so getting a message will cause the widget to re-render.

 

//To send a message, with a widget id of 'widget_id', use:

//Dispatch- This should be your preferred method, as it is immutiable.
getAppStore().dispatch(appActions.widgetStatePropChange('widget_id','nameOfMessage', message))

//MutableStoreManager- Use this only if you need to send a complex object.
MutableStoreManager.getInstance().updateStateValue('widget_id', 'nameOfMessage', message)

//To read a message

//From Dispatch:
props.stateProps?.nameOfmessage

//From MutableStoreManager:
props.mutableStateProps?.nameOfmessage

 

I think that covers all of the basic information you need to know about React to use Experience Builder Developer Edition and yes, the React iceberg goes much deeper.

Good Luck and May Your Toes Never Be Broken!

14 Comments
About the Author
A frequently confused rock-hound that writes ugly, but usually functional code.