Once you boil it down, React isn’t a super-complicated development platform. It uses Javascript, which as been around for about as long as the Web has and is damn near ubiquitous. A React component is just a function that can be called from another function, and most components feature a way to maintain state, maybe some processing directives to maintain flow of control, some event handlers, and the HTML output. Beyond that, we can sex it up as much as we want with external libraries that obfuscate the heavy lifting and keep our presentation components clean or bring in third party libraries that will do that stuff for us.

I have found that the hardest thing to grasp about React is reconciling the one-way communication flow and how to set up multiple components in a way that data can flow between them. Technically there is no one way to handle this. I have written about hooks and higher order components which collect and expose data across component bounds, and there’s always the power-utilities like Redux which takes state management to a whole new level. But as I’ve been working with React at work, I’ve had to come up with a relatively simple way to get two components to communicate despite the one-way flow of information.

This method is known in some parts as a “higher order component”, but we’re looking at it specifically to facilitate the movement of data both up and down the component stack.

Components and Separation of Concern

In the development world, folks like to talk about separation of concern. Basically this means that when creating “something”, that “something” shouldn’t do more than it’s fair share of work. For example, we don’t want a button on a web page to actually contain the logic needed to submit a form; the button should only concern itself with it’s visual presentation between states (normal, hover, pressed, disabled, etc.) and maybe what method it calls when clicked. We should never embed the actual submission logic into a button because that’s putting too much power and responsibility into a lowly button element.

The purpose of a React component, then, is two-fold. The first is the separation of concern and how a component should only handle what it’s theoretically designed to handle. The second is that components should be reusable. We might not actually re-use a component, but the joy of React is that, with a little foresight, we can develop a component that is flexible enough to reuse. A good example here is that button we mentioned previously. We could create a pretty fancy component that does some wild visual stuff, and then use it several times throughout the site. If we need to update the component, we can do so once and it’ll take effect everywhere that component is used.

Granularity and Over-Separating

Once the project starts getting larger, everything starts to look like a component that needs its concerns separated. While our previously mentioned super-button might be a decent candidate for being a reusable component, why do we need a button with advanced AI? A good application of CSS can do a lot for a simple button input control. Same with a textbox. Do we need to create a separate React component for every form element? Absolutely not!

The rule of thumb that I’ve been using in this project has been:

  1. Does this have non-standard functionality? If so, it should be a component.
  2. Is there something about this element that should be self-contained? If so, it should be a component.
  3. Would I want the functionality of this to be used elsewhere? If so, it should be a component.

When building a search form, designing the form in the old school HTML way is perfectly fine. Input text, select lists, radio buttons, and normal buttons that don’t do anything different from their basic purposes do not need to inhabit their own files. In fact, doing so adds unnecessary complexity to the project.

However, if there’s a case were we want to combine a button and a text input for some special functionality, or we have to construct our own take on a select list in order to accommodate CSS or some funky select list item format, then we would most certainly want to create our own components.

Component Communication

With these factors in mind, let’s consider a pretty common business-app scenario: the search and results page. In old school development, we might include both the search form and the results within the same page/view. In the past, I have combined C# MVC with jQuery to create a single page with the form defined at the top, and a partial table definition at the bottom which would be filled with the results of a jQuery AJAX call to the back-end.

C# MVC with jQuery – Single page search and results

Other ways to handle this depend on your skill-set, your coding language, and your target platform, meaning there’s no one single way to approach this kind of task. Considering how many sites out there have search features, it’s probably one of the most implemented systems out there and I bet there are as many ways of handling it.

In this example, use of the Submit button would send the form data to a back-end C# process via jQuery AJAX. When the resulting dataset is returned to the front-end, I’d loop through the results and build the <tbody> contents by adding to the DOM. All in all, this would be a single page, probably with an external Javascript file that contains the form post and return handling, but as you can see it doesn’t separate the concerns very well. Although we could argue that the form and the results are a single concern — we can’t search if we don’t have visibility to the results, and we don’t have results without having searched — one question can help determine if we might be better off pulling these two systems apart: What if I want to do something else with the form, or get data into the results from somewhere else?

Communication Woes

One of React’s major stumbling-blocks is that data only flows down through the component stack. That means if we create a component to represent “a home page” and want to include a “search component” within that home page component, we’ll end up with something like this:

const HomePage = (props) -> {
   //State, destructuring, and other prep processing

   return (
      <div>
         {/* Other home page output here */}
         <SearchBox />
      </div>
   )
}
export { HomePage }

Encapsulating the search functionality is a good example of something that should have its own component. It has a specific function beyond the standard text box entry, and we might want to re-use it elsewhere on the site without copying the functional code inside the component over and over.

The above pseudocode doesn’t really showcase our problem with communication, but if we add a component designed to display the search results, we start to see what kinds of issues we run into:



const HomePage = (props) -> {
   //State, destructuring, and other prep processing

   return (
      <div>
         {/* Other home page output here */}
         <SearchBox />
         <SearchResults />
      </div>
   )
}
export { HomePage }

If the <SearchBox /> component has encapsulated functionality, and if data only flows down, how can we get results from the searchbox into the <SearchResults /> component?

Manager Components, State, and Properties

In this case, we start with a management component. React has a cool little feature that if a folder contains a file named index.js, it has the authority to “speak” for all other components in the folder, assuming other components are imported into index.js. Our management component behaves as the “page”, to present other components, but more importantly it deals with the state. That makes index.js an obvious candidate for managing the flow of data.

State in React is important; if we can do without it, then we’re best not to use it for the sake of using it, but because of the way React’s native state-handling works, it’s beneficial to deploy it for moving data between components.

In the above diagram we see that index.js has two child components, inputform.js and results.js. By splitting the search form and the results into two components, we’re addressing the separation of concern.

The problem is, we need to get the data from the search form over to its sibling results display. In order to accomplish this, we need to set up a pipeline. We’ll look at it in the order in which we expect a user to interact with it — search, management, results.

Search Component

const InputForm = (props) => {
   const [ formVals, setFormVals ] = useState(defaultFormVals);

   const { onResultsReceived } = props;

   const handleChange = (e) => {
      const { name, value } = e.target;
      setFormvals(prev=>({
         ...prev,
         {[name]:value}
      }));
   }

   const handleSubmit = (e) => {
      e.preventDefault();

      //Submit the search, passing formVals to whatever method you use 
      //   to query a datastore. 
      axios.get('API_ENDPOINT_WITH_QSTRING')
      .then(results=>{
         if (results.status === 200) {
            //Successful return. Check results.data for actual data
            onResultsReceived(results.data);
         }
      })
      .catch(err=>{
         console.log(err);
         //Plus more robust error logging and surfacing as needed.
      });

   return(
      <form onSubmit={handleSubmit}>
         <input type="text" name="FirstName" onChange={handleChange} value={formVals?.FirstName} />
         ...
      </form>
   )
}

export { InputForm }

Our search form begins with the definition of formVals. This state variable holds all of the changes that the user makes when filling out the search form. This makes our standard form controls into controlled elements. When the content of a textbox changes, the onChange event is fired. The handleChange function will take the name of the element and the value of the element and will update the formVals state variable. Back on the textbox, the value of the element is equal to whatever is in the formVals.FirstName property. This cycle between updating and reading to and from state puts the lifecycle of the textbox under the “control” of state.

Next we’re destructuring a named property, onResultsReceived. This is passed into the search component from the management component and we’ll look at that in a bit.

handleSubmit is our form handler. When the user submits the search, the formVals state contents are passed to whatever method we have that queries whatever datastore we’re using. In the example, I’m using Axios to asynchronously call an API, passing the form elements in the querystring (use POST to pass in body). The key to this handler lies in the return results. Once we’ve determined that we have actual data, we will pass that data to the destructured method passed in from the management component.

To recap:

  1. The search component tracks it’s own form values
  2. It also submits form values and receives results to and from some data storage mechanism
  3. When acceptable search results are received, they are passed up to the management component.

Management Component

//Import search and results components here.

const Index = (props) => {
   const [ results, setResults ] = useState(null);

   const handleResults = (results) => {
      setResults(results);
   }

   return (
      <div>
         <InputForm onResultsReceived={handleResults} />
         <SearchResults resultData={results} />
      </div>
   )
}

export t{ Index }

This is a very basic pseudo-implementation of the management component index.js. After importing both the search and results components, we once again set up a state variable. Here, results will store the results passed up from the InputForm search component. We do this via the handleResults method. This method is defined within the management component and is passed to the search component. By doing this, calling that method (via the alias onResultsReceived within the search component), it’s like we’re calling the original. The argument results is going to be the search results received from our async API call.

The async nature of the API call bears noting. If you’re not used to async calls (and even if you are) then they can be a real pain in the ass because we might not always have access to data when and where we might think we do. If you remember back in the pseudocode for the InputForm example above, the value of the textbox was assigned like this:

formVals?.FirstName

This is a cool shortcut that Javascript affords us. It’s saying that if formVals is not null, then use the value of FirstName from that object. If we tried to access FirstName while formVals was null, we’d get an error. We can also use the implied boolean of constructs such as this to determine if we have values after an async process:

if (results.data) { ... //We have a value } 

If results.data is not null, then our if will evaluate to true and our code will fire.

I mention this here because it’s important to the next part: the use of the returned data in the SearchResults component. Notice that we’re passing the result data up from the search form to the management component and are storing it in a state variable. This is useful because we can refer to the contents of this state variable over time, preventing us from having to re-query the database with the same form values when we want to do something like, say, sort results in our current data set. But it has another benefit, which is triggering a refresh of any component which refers to results state variable.

To recap:

  1. Index controls the state of the search process
  2. We define a handler and pass it to the search form, which calls it to return the search results to the manager.
  3. The handler updates the state variable with the results passed up from the child component.

Results Component

const SearchResults = (props) => {

   const { resultData } = props;

   let rows = [];
   useEffect(()=>{
      if (resultData)
         resultData.map(itm=>{
            return rows.push(<tr key={itm.ID}><td>{itm.FirstName}</td><td>...other fields</td></td>)
         })
   },[resultData]);

   return (
      <table>
         <thead><tr><th>...Headers</th></tr></thead>
         <tbody>{rows}</tbody>
      </table>
   )
}
export { SearchResults }

We’re not tracking state here because this component is merely a display component. We accept the passed in value of resultData which should contain info passed up to the management component from the InputForm component, which is then passed down to our SearchResults component. Because we stored it in state within the index component, when it updates it’ll update the instance passed to the results display. We handle this using the useEffect hook. By specifying the variable resultData as the last parameter of the hook, the hook will fire whenever the value of resultData changes.

Within the useEffect, we are looping through the data using map. Each iteration adds a new table row to an array called rows. We reference this variable between the <tbody> tags of the table that we are returning from the component.

To recap:

  1. Index has passed it’s state variable to the SearchResults component as resultData property.
  2. We use the useEffect hook to build our display when the value of the passed-in property has changed
  3. The array of table rows is incorporated into the returned HTML. When it has values, we’ll have rows.

Reduce, Reuse, Recycle

This is a very specific example of the interaction between a search form, a state manager, and a results display which might lead us to think that if we’re going to have this relationship then why divide up the functionality into different components? Aside from the “separation of concerns” that will earn brownie points, we could turn around an use the search form to send results to a completely different view elsewhere in the app. The search form only manages the form values and does the query, but doesn’t tell the app what to do with the results.

On the other hand, the results display could be updated to deal with the data fields dynamically, making the results display component completely divested of the data that’s passed to it. With additional foresight, the results display component could be reworked to accept any data collection to return a consistently formatted, functional result set. One reason we might want to do this would be to handle sorting of data within the table display. If we can include that functionality in the results display component, then we could deploy it to handle any kind of data, and wouldn’t have to re-write the support code every time — even between projects!

Note that this is a very simple example. In my project, I actually have a three component communication design, which unfortunately ramps up the difficulty significantly on account of which component is updating, which components are consuming, and the fact that some components do both, but the separation of concerns and attention to what requires a component and what does not allows for an effective structure that passes data both up and down the component stack.

Scopique

Owner and author.

1 Comment

  • Picking Something to Learn – Time to Loot

    September 10, 2021 - 7:00 AM

    […] project to resurrect West Karana, UltrViolet’s building of a static blog infrastructure, Scopique’s adventures learning React and Pete’s discussion of his own history in […]

Leave a Reply

Your email address will not be published. Required fields are marked *