The new Display component will be a purely presentational one with no state or interactions of its own. That’s normal. Not every React component has to have stateful hooks or be interactive.

The responsibility of the Display component is to simply display a value that it will receive as a prop. For example, the fact that a pre element was used to host the value is part of that responsibility. Other components in this application have no say about that!

However, you should always try to make the first argument to ReactDOM.render a single component call instead of the nested tree that we just did. This is essentially a code quality preference. It forces you into thinking about your components hierarchy, names, and relations. Let’s do that next.

The empty tag will get transpiled into the React.Fragment syntax. I’ll use this syntax to continue with the example.

This case is so common in React that the JSX extension has a shortcut for it. Instead of typing React.Fragment , you can just use an empty tag <></> .

The React API supports this nesting. In fact, React has a special object if you need to enclose multiple adjacent elements like this without introducing a new DOM parent node. You can use React.Fragment :

Another option is to make the sibling React elements the children of another React element. For example, we can just enclose them in a div element.

This is usually a good solution when all the elements you’re rendering are coming from a dynamic source. However, it’s not ideal for the case we’re doing here.

First, you can pass an array of elements to ReactDOM.render and insert into that array as many React elements as you wish.

Adjacent elements can’t be rendered like this in React because each of them gets translated into a function call when JSX gets converted. You have a few options to deal with this issue.

We now have two elements to render: Button and Display . We can’t render them directly next to each other like this:

The top-level component

Let’s introduce a top-level component to host both the Button and Display components. The question now is: what should we name this new parent component?

Believe it or not, naming your components and their state/props elements is a very hard task that will affect the way these components work and perform. The right names will force you into the right design decisions. Take some time and think about every new name you introduce to your React apps.

Since this new parent component will host a Display with a Button that increments the displayed count, we can think of it as the count value manager. Let’s name it CountManager .

const CountManager = () => { return ( <> <Button /> <Display /> </> ); }; ReactDOM.render(<CountManager />, mountNode);

Since we’re going to display the count’s value in the new Display component, we no longer need to show the count’s value as the label of the button. Instead, we can change the label to something like "+1".

const Button = () => { return ( <button onClick={() => console.log('TODO: Increment counter')}> +1 </button> ); };

Note that I’ve also removed the state element from the Button component because we can’t have it there anymore. With the new requirement, both the Button and Display components need access to the count state element. The Display component will display it and the Button component will update it. When a component needs to access a state element that’s owned by its sibling component, one solution is to "lift" that state element one level up and define it inside their parent component. For this case the parent is the CountManager component that we just introduced.

By moving the state to CountManager , we can now "flow" data from parent to child using component props. That’s what we should do to display the count value in the Display component:

const Display = ({ content }) => ( <pre>{content}</pre> ); const CountManager = () => { const [count, setCount] = useState(0); return ( <> <Button /> <Display content={count} /> </> ); }; ReactDOM.render(<CountManager />, mountNode);

Note how in CountManager I used the exact same useState line that was in the Button component. We are lifting the same state element. Also note how when I flowed the count value down to the Display component via a prop, I used a different name for it ( content ). That’s normal. You don’t have to use the exact same name. In fact, in some cases, introducing new generic names are better for children component because they make them more reusable. The Display component could be reused to display other numeric values besides count .

Parent components can also flow down behavior to their children, which is what we need to do next.

Since the count state element is now in the CountManager component, we need a function on that level to handle updating it. Let’s name this function incrementCounter . The logic for this function is actually the same logic we had before in the handleClick function in the Button component. The new incrementCounter function is going to update the CountManager component count state to increment the value using the previous value:

const CountManager = () => { // .... const incrementCounter = () => { setCount(count + 1); } // ... };

The onClick handler in the Button component now has to change. We want it to execute the incrementCounter function that’s in the CountManager component but a component can only access its own functions. So, to make the Button component able to invoke the incrementCounter function in the CountManager component we can pass a reference to incrementCounter to the Button component as a prop. Yes, props can hold functions as well, not just data. Functions are just objects in JavaScript and just like objects you can pass them around.

We can name this new prop anything. I’ll name it clickAction and pass it a value of incrementCounter , which is the reference to the function we defined in the CountManager component. We can use this new passed-down behavior directly as the onClick handler value. It will be a prop for the Button component:

const Button = ({ clickAction }) => { return ( <button onClick={clickAction}> +1 </button> ); }; // ... const CountManager = () => { // ... return ( <div> <Button clickAction={incrementCounter} /> <Display content={count} /> </div> ); };

Something very powerful is happening here. This clickAction property allowed the Button component to invoke the CountManager component’s incrementCounter function. It’s like when we click that button, the Button component reaches out to the CountManager component and says, "Hey Parent, go ahead and invoke that increment counter behavior now".

In reality, the CountManager component is the one in control here and the Button component is just following generic rules. If you analyze the code as it is now, you’ll realize how the Button component has no clue about what happens when it gets clicked. It just follows the rules defined by the parent and invokes a generic clickAction . The parent controls what goes into that generic behavior. This follows the concept of responsibility isolation. Each component here has certain responsibilities and they get to focus on that.

Look at the Display component for another example. From its point of view, the count value is not a state. It is just a prop that the CountManager component is passing to it. The Display component will always display that prop. This is also a separation of responsibilities.

As the designer of these components, you get to choose their level of responsibilities. For example, we could have made the responsibility of displaying the count value part of the CountManager component itself and not use a new Display component for that.

The CountManager component has the responsibility of managing the count state. That’s an important design decision that we made and it’s one you’re going to have to make a lot in a React application. Where to define the state?

The practice I follow is to define a state element in a shared parent node that’s as close as possible to the all the children who need to access that state element. For a small application like this one, that usually means the top-level component itself. In bigger applications, a sub-tree might manage its own state "branch" rather than relying on global state elements that are defined on the top-level root component.

The top-level component is popularly used to manage shared application state and actions because it’s parent to all other components. Be careful about this design because updating a state element on the top-level component means that the whole tree of components will be re-rendered (in memory).

Here’s the full code for this example so far: