React's approach

Many modern libraries and frameworks have sought to make this a less burdensome process by abstracting the DOM reconciliation process away behind a declarative interface. A good example of this is React, which allows you to declare your DOM tree declaratively using its JSX syntax within your JavaScript. JSX looks like regular HTML with the addition of interpolation delimiters ({...}) where regular JavaScript can be written to express data.

Here, we are creating a component that produces a simple <h1> greeting populated with an uppercase name:

function LoudGreeting({ name }) {
return <h1>HELLO { name.toUpperCase() } </h1>;
}

The LoudGreeting component could be rendered to <body> like so:

ReactDOM.render(
<LoudGreeting name="Samantha" />,
document.body
);

And that would result in the following:

<body>
<h1>HELLO SAMANTHA</h1>
</body>

We might implement a ShoppingList component in the following way:

function ShoppingList({items}) {
return (
<ul>
{
items.map((item, index) => {
return <li key={index}>{item}</li>
})
}
</ul>
);
}

And then we could render it in the following way, passing our specific shopping list items in our invocation of the component:

ReactDOM.render(
<ShoppingList items={["Bananas", "Apples", "Chocolate"]} />,
document.body
);

This is a simple example but gives us an idea of how React works. The true magic of React is in its ability to selectively re-render the DOM in reaction to changes in data. We can explore this in our example by changing data in reaction to a user action.

React and most other frameworks give us a straightforward mechanism of event-listening so that we can listen for user events in the same manner as we would conventionally. Via React's JSX, we can do the following:

<button
onClick={() => {
console.log('I am clicked!')
}}
>Click me!</button>

In our case of the shopping list problem domain, we want to create <input />, which can receive new items from users. To accomplish this, we can create a separate component called ShoppingListAdder:

function ShoppingListAdder({ onAdd }) {
const inputRef = React.useRef();
return (
<form onSubmit={e => {
e.preventDefault();
onAdd(inputRef.current.value);
inputRef.current.value = '';
}}>
<input ref={inputRef} />
<button>Add</button>
</form>
);
}

Here, we are using a React Hook (called useRef) to give us a persistent reference that we can re-use between component renders to reference our <input />.

React Hooks (typically named use[Something]) are a relatively recent addition to React. They've simplified the process of keeping persistent state across component renders. A re-render occurs whenever our ShoppingListAdder function is invoked. But useRef() will return the same reference on every single call within ShoppingListAdder. A singular React Hook can be thought of as the Model in MVC.

To our ShoppingListAdder component, we are passing an onAdd callback, which we can see is called whenever the user has added a new item (in other words, when the <form> submits). To make use of a new component, we want to place it within ShoppingList and then respond when onAdd is invoked by adding a new item to our list of food:

function ShoppingList({items: initialItems}) {

const [items, setItems] = React.useState(initialItems);

return (
<div>
<ShoppingListAdder
onAdd={newItem => setItems(items.concat(newItem))}
/>
<ul>
{items.map((item, index) => {
return <li key={index}>{item}</li>
})}
</ul>
</div>
);
}

As you can see, we are using another type of React Hook called useState to persist the storage of our items. initialItems can be passed into our component (as an argument) but we then derive a set of persistent items from these that we can mutate freely across re-renders of our component. And that's what our onAdd callback is doing: it is adding a new item (entered by the user) to the current list of items:

Calling setItems will, behind the scenes, invoke a re-render of our component, causing <li>Coffee</li> to be appended to the live DOM. Creations, updates, and deletions are all handled similarly. The beauty of abstractions like React is that you don't need to think of these mutations as distinct pieces of DOM logic. All we need to do is derive a component/DOM tree from our set of data and React will figure out the precise changes needed to reconcile the DOM.

To ensure we understand what's going on, when a piece of data (state) is changed via a Hook (for example, setItems(...)), React does the following: 

  1. React re-invokes the component (re-renders)
  2. React compares the tree returned from the re-render with the previous tree
  3. React makes the essential granular mutations to the live DOM for all of the changes to be reflected

Other modern frameworks borrow from this approach as well. One nice side-effect of DOM reconciliation mechanisms built into these abstractions is that, via their declarative syntax, we can derive a deterministic tree of components from any given data. This is in stark contrast to the imperative approach, within which we must manually select and mutate specific DOM nodes ourselves. The declarative approach gives us a functional purity that enables us to produce outputs that are deterministic and idempotent. 

As you may recall from Chapter 4SOLID and Other Principles, functional purity and idempotence give us standalone testable units of predictable input and output. They allow us to say X input will always result in Y output. This transparency aids tremendously in both the reliability and the comprehensibility of our code.

Building large web applications, even with the reconciliation puzzle out of the way, is still a challenge. Every component or view within a given page needs to be populated with its correct data and needs to propagate changes. We'll be exploring this challenge next.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.235.23