How to handle component interaction in React

Dealing with component interaction is a key aspect of building applications in React. Here’s a look at the options.

How to handle component interaction in React
Thinkstock

Every React app is composed of interacting components. How these components communicate is an essential aspect of the UI architecture. As applications grow larger and more complex, component interaction becomes even more important.

React provides several methods for handling this need, each with its appropriate use cases. Let’s begin with the simplest approach, parent-to-child interaction.

Parent-to-child with props

The simplest form of communication between components is via properties — usually called props. Props are the parameters passed into child components by parents, similar to arguments to a function.

Props allow for variables to be passed into children, and when values are changed, they are automatically updated in the child, as in Listing 1.

Listing 1. Props (class-based)

function App(){
  return <div>
    <AppChild name="Matt" />
    </div>
}

function AppChild(props){
  return <span>
      My name is {props.name}
    </span>
}

ReactDOM.render(<App />, document.getElementById('app'));

Listing 1 shows how props are handled in a function-based component tree. The process is similar with classes. The function-based example shows off the more streamlined syntax of the functional style. You can see this code in action here.

Child-to-parent with function props

Listing 1 allows for values to be passed from parent to child. When a child needs to update the parent as to changes, they cannot just modify properties. Children cannot update props.

If you attempt to directly modify a prop on a child, you will see the following type of error in the console:

Uncaught TypeError: Cannot assign to read only property 'foo' of object '#<Object>'

Instead, a parent can pass in a functional prop, and the child can call that function. Such functional props are a kind of event-oriented programming. You can see this in action in Listing 2.

Listing 2. Functional props

function App(){
  const [name, setName] = React.useState("Matt");
  return <div>
      <AppChild name={name} onChangeName={()=>{setName("John")}}/>
    </div>
}

function AppChild(props){
  return <span>
      My name is {props.name}
      <button onClick={props.onChangeName}>Change Name</button>
    </span>
}

ReactDOM.render(<App />, document.getElementById('app'));

Listing 2 introduces useState for managing state. This is a simple mechanism about which you can learn more here. The essence of the functional prop is that when the button is clicked, the function passed in by the App component is executed. Thus, child-to-parent communication is achieved. You can see this code live here.

In general, the concept to keep in mind is this: Props flow down to children, events flow up to parents. This is a valuable design principle that helps to keep applications organized and manageable.

Passing information up to parents

It often happens that child components need to pass arguments up along with their events. This can be achieved by adding arguments to the functional prop callback. This is handled as seen in Listing 3.

Listing 3. Passing arguments to functional props

function App(){
  const [name, setName] = React.useState("Matt"); //test
  return <div>
      <AppChild name={name} onChangeName={(newName)=>{setName(newName)}}/>
    </div>
}

function AppChild(props){
  return <span>
      My name is {props.name}
      <button onClick={()=>props.onChangeName("Bill")}>Change Name</button>
    </span>
}

ReactDOM.render(<App />, document.getElementById('app'));

Notice in Listing 3 the line onClick={()=>props.onChangeName("Bill")}. Here we use the arrow syntax to create an anonymous function that includes the argument we want. It’s a simple matter to also pass a variable that is modified by the component itself, with syntax like: onClick={(myVar)=>props.onChange(myVar)}. This code can be seen live here.

As a side note, inline arrow functions as event handlers as seen here are sometimes criticized on the grounds of performance, although this may be overblown.

Function props and React Router

Another important use case is for passing arguments across the React Router. Listing 4 provides an example of how this is achieved.

Listing 4. Passing functional props through Router

// In the route definition:
<Route path=’/foopath’ render={(props) => <Child {…props} />} />
// In the child component:
<Route appProps={{ onTitleChange }} />

In essence, Listing 4 is allowing for the direct pass-through of the properties by overriding the render of the route.

Sibling communication

The features we’ve seen so far offer the ability to handle sibling communication. This is known in the React docs as “lifting up state.”

The idea here is that when children at the same level of the component tree must share state, that state is pushed up to the parent. The parent then shares the state to the children who need it via props. The children raise events to update that state at the parent, which will automatically be reflected across the shared properties.

React Context API

Another option proffered by React itself is the Context API. The Context API is designed to manage simple, globally interesting values. That is to say, values that are used by many components across the app.

The example given in the docs is a theme setting. Many components will be interested in this setting (in order to reflect the proper theme), which would be very unwieldy to pass around with props.

The Context API is not intended for dealing with complex application data. It is really targeted specifically for avoiding complex prop handling in deeply nested components. A brief example is seen in Listing 5.

Listing 5. Context API

// defining the context value
<ThemeContext.Provider value="dark">  
// Consuming the context value later on
<Button theme={this.context} />;  

Centralized state with Redux

More complex applications may merit more complex state architectures. The most common library for handling this in React remains Redux. Redux is not simply a centralized store: It is an opinionated and structured eventing system.

The core idea in Redux is that components raise events (known in Redux as actions) via specialized objects called dispatchers. These action events are observed by reducers, which then apply the action to the state. Components in the view are then automatically updated to reflect the state.

You can see from this brief description that Redux introduces quite a bit of complexity and formality into your application. This should be balanced carefully with the benefits of structure and understandability when using Redux.

Other centralized stores

Other approaches exist to managing centralized store, including MobX and rolling your own. Although these solutions may offer advantages over Redux, they must be weighed against the advantage that Redux’s popularity offers, namely familiarity and the availability of developers who understand it.

React offers very powerful and simple component interaction via props and function props. This approach can break down in larger, more complex applications. Leveraging more sophisticated options like the Context API and Redux can address these more complex needs.

Copyright © 2021 IDG Communications, Inc.