Combine React Views with External Models

Backbone (http://backbonejs.org) is a popular library to manage the model layer in JavaScript application. Backbone models are objects that emit events when you modify them. This system works backwards compared to React, where you explicitly call setState to trigger the updates. Backbone also has its own view utilities, but I want to focus on reusing your Backbone models when you migrate to React as the view. How do you proceed? You’ll use a smart container component that manages the Backbone models and two dumb components that display the data and handle user input. This way most of your React components don’t contain Backbone-specific code, and you centralize in a single component handling the events emitted by Backbone collections and models.

To demonstrate this approach, you’ll build a little interface that displays a list of contacts and allows the user to add new ones. Make a new directory for this example; to build this project, reuse webpack.config.js, package.json, .babelrc, and index.html from the previous examples.

First, we’ll build our presentational components. Start with the component to display a contact called ContactCard. ContactCard knows nothing about Backbone. It just takes its data from its contact prop. contact is a plain JavaScript object, with a name, address, and zip property. The component manages no state of its own and doesn’t require lifecycle hooks, so create a function component in ContactCard.js in the src directory:

 import​ React from ​'react'​;
 
 function​ ContactCard({ contact }) {
 return​ (
  <dl>
  <dt>Name</dt>
  <dd>​{​contact.name​}​</dd>
  <dt>Address</dt>
  <dd>​{​contact.address​}​</dd>
  <dt>Post code/ ZIP code</dt>
  <dd>​{​contact.zip​}​</dd>
  </dl>
  );
 }
 
 export​ ​default​ ContactCard;

Next, create a component that displays the whole list called ContactList that creates a new ContactCard element for each of the contacts in its own props. ContactList doesn’t know about Backbone either. Its contacts prop is a plain JavaScript array of plain objects. Create ContactList in a file named ContactList.js in the src directory:

 import​ React from ​'react'​;
 import​ ContactCard from ​'./ContactCard'​;
 
 function​ ContactList({ contacts }) {
 const​ contactCards = contacts.map(contact =>
  <ContactCard contact=​{​contact​}​ key=​{​contact.cid​}​ />
  );
 return​ (
  <div>
  <h1>Contacts</h1>
 {​contactCards​}
  </div>
  );
 }
 
 export​ ​default​ ContactList;

We use map to create an array of ContactCard elements.

For the new contact form, first create a TextInput component. This will prevent duplication when you create the form to handle the contact fields. TextInput accepts two props: the input label text and a function that fires when the <input> field changes, to notify TextInput’s parent component about the new data.

 import​ React from ​'react'​;
 
 function​ TextInput({ label, changed }) {
 return​ (
  <label>
 {​label​}
  <input onChange=​{​changed​}​ />
  </label>
  );
 }
 
 export​ ​default​ TextInput;

Then assemble the input fields into a ContactForm component. Create a new file in the src directory named ContactForm.js. The constructor initializes a form object to store the form data. save will be called when the user clicks the submit button and delegates to the onSubmit function handed in by the parent component.

 import​ React from ​'react'​;
 import​ TextInput from ​'./TextInput'​;
 
 class​ ContactForm ​extends​ React.Component {
  constructor() {
 super​();
 this​.form = {};
 this​.save = ​this​.save.bind(​this​);
  }
 
  save() {
 this​.props.onSubmit(​this​.form);
  }
 }
 
 export​ ​default​ ContactForm;

Add an update function that takes a string and returns a new function that updates that particular key in a form object that’s part of the component instance. This saves some code duplication when we handle the input fields changes. If you need to create a lot of forms, you can take a look at dedicated libraries like formsy-react (https://github.com/christianalfoni/formsy-react).

 update(key) {
 return​ ​function​(event) {
 this​.form[key] = event.target.value;
  }.bind(​this​);
 }
 render() {
 return​ (
  <form>
  <h2>Add contact</h2>
  <TextInput label=​"Name"​ changed=​{​​this​.update(​'name'​)​}​ />
  <TextInput label=​"Address"​ changed=​{​​this​.update(​'address'​)​}​ />
  <TextInput label=​"ZIP"​ changed=​{​​this​.update(​'zip'​)​}​ />
  <TextInput label=​"Email"​ changed=​{​​this​.update(​'email'​)​}​ />
  <button type=​"button"​ onClick=​{​​this​.save​}​>Save</button>
  </form>
  );
 }

This form component takes a single prop, a function that gets called with this.form as an argument when the user submits the form. The parent component receives the form data and can do what it wants with it.

That’s it for the presentational components. Now let’s define the Backbone models. You can install the latest Backbone version from npm:

 $ ​​npm​​ ​​i​​ ​​--save​​ ​​backbone

You’ll also need to install jQuery, a Backbone dependency:

 $ ​​npm​​ ​​i​​ ​​--save​​ ​​jquery

The Backbone package exports a single Backbone object. Backbone.Model.extend creates a new Backbone model. Create a minimal Backbone model that represents a single contact:

 import​ Backbone from ​'backbone'​;
 
 const​ Contact = Backbone.Model.extend({});
 
 export​ ​default​ Contact;

We’ll leave the arguments for the model empty as we don’t need to add any methods to the model for this example. You can continue to use all of Backbone’s Model facilities to perform validation and fetch data if that’s practical for you. You’ll also need a collection. Backbone collections store lists of models. In ContactCollection.js, create a minimal Backbone collection that expects a model type of Contact. Specifying the model type allows you to turn plain JSON objects added to the collection into Contact instances automatically.

 import​ Backbone from ​'backbone'​;
 import​ Contact from ​'./Contact'​;
 
 const​ ContactCollection = Backbone.Collection.extend({
  model: Contact
 });
 
 export​ ​default​ ContactCollection;

Finally, build the container component to manage the Backbone collection. In the src directory, create a new class component named App and initialize its state to an empty array:

 class​ App ​extends​ React.Component {
  constructor(props) {
 super​(props);
 this​.state = { data: [] };
  }
 }

To retrieve the form data, create a function that takes an object and adds it to the collection:

 addElement(item) {
 this​.props.collection.add(item);
 }

To access props in the constructor, you need to define a props parameter and pass it to super. this.props.collection points to a ContactCollection you will pass to the App element as a prop. item is a plain JavaScript object. Since we defined the model property when we created the ContactCollection collection type, add automatically converts item to a Contact instance. Backbone collections emit the add event when you add an item. When add fires, call a function that updates the App’s component state with the new contents of the collection. In this way, you make use of the normal React rendering mechanism. When a new item gets added to the collection, replace the current state with the collection contents.

 constructor(props) {
 super​(props);
 this​.state = { data: [] };
»this​.addElement = ​this​.addElement.bind(​this​);
»this​.props.collection.on(​'add'​, () => {
»this​.setState(() => ({ data: ​this​.props.collection.toJSON() }));
» });
 }

Despite its name, toJSON returns an array of plain JavaScript objects. Remember to call bind on addElement so this keeps pointing to the App component.

In the render function, pass addElement as the onSubmit prop to ContactForm.

 render() {
 return​ (
  <div>
  <ContactList contacts=​{​​this​.state.data​}​ />
  <ContactForm onSubmit=​{​​this​.addElement​}​ />
  </div>
  );
 }

ContactForm calls addElement with the contents of the form as a simple object; it’s handy that add also accepts JSON objects instead of Backbone.Model instances.

We’ve got both the Backbone models and the React components ready, so let’s run our application. We’ll start with an empty contact list. In index.js, render the App element with a new empty ContactCollection:

 import​ ReactDOM from ​'react-dom'​;
 import​ React from ​'react'​;
 import​ ContactCollection from ​'./ContactCollection'​;
 import​ App from ​'./App'​;
 
 const​ collection = ​new​ ContactCollection();
 
 ReactDOM.render(
  <App collection={collection} ​/>​​,
  document.getElementById(​'app'​)
 );

Let’s run the application. Run

 $ ​​npm​​ ​​start

and visit the application in your web browser.

images/contacts_ui.png

Fill in the form and click Save to add a new contact.

Listening to the change event allows the App component to re-render even if a legacy Backbone view modifies the collection.

Limit Backbone-specific code to the top component and do not listen for model changes across all your components. Subscribe to model events in the top component and update this.state whenever the model changes. This will limit event-spaghetti, where events are firing from all over the place and you cannot trace the interaction that triggered them. Use toJSON to get your data as plain JavaScript objects in this.state and pass it to the other components via props. As you deal with plain JavaScript data structures, your code is more readable and allows you to eventually move away from Backbone completely.

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

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