Git Product home page Git Product logo

connect-backbone-to-react's Introduction

connect-backbone-to-react travis npm

Connect Backbone Models and Collections to React.

Usage

npm install connect-backbone-to-react or yarn add connect-backbone-to-react in your React/Backbone project. See code samples below to how to integrate into your code.

Example

Edit connectBackboneToReact

connectBackboneToReact

const UserModel = Backbone.Model.extend();
const UserCollection = Backbone.Collection.extend({ model: UserModel });

const userInstance = new UserModel({ name: "Harry", laughs: true });
const anotherUserInstance = new UserModel({ name: "Samantha", laughs: false });
const userCollection = new UserCollection([userInstance, anotherUserInstance]);

class MyComponent extends React.Component {
  render() {
    return (
      <div>
        <p>My user laughs: {this.props.doesUserLaugh ? "yes" : "no"}</p>
        <button
          onClick={() => this.props.setUserLaughs(!this.props.doesUserLaugh)}
        >
          Toggle Laughing User
        </button>
        <h4>All Users</h4>
        <ul>
          {this.props.users.map(user => (
            <li key={user.name}>{user.name}</li>
          ))}
        </ul>
      </div>
    );
  }
}

// Maps Models to properties to give to the React Component. Optional.
// Default behavior is to call `.toJSON()` on every Model and Collection.
// Second argument are props given to the React Component.
const mapModelsToProps = (models, props) => {
  const { user, allUsers } = models;
  const { showOnlyLaughingUsers } = props;

  // Everything returned from this function will be given as a prop to your Component.
  return {
    doesUserLaugh: user.get("laughs"),
    users: showOnlyLaughingUsers
      ? allUsers.toJSON().filter(user => user.laughs === true)
      : allUsers.toJSON(),
    setUserLaughs(newVal) {
      user.set("laughs", newVal);
    }
  };
};

// Options.
const options = {
  // Should our event handler function be wrapped in a debounce function
  // to prevent many re-renders.
  debounce: false, // or `true`, or a number that will be used in the debounce function.

  // Define what events you want to listen to on your Backbone Model or Collection
  // that will cause your React Component to re-render.
  // By default it's ['all'] for every Model and Collection given.
  events: {
    user: ["change:name", "change:laughs"]
    // You can disable listening to events by passing in `false` or an empty array.
  },

  // Define what modelTypes you expect to be contained on your `modelsMap` object.
  // Useful for validating that you'll be given what model type you expect.
  // Uses instanceof, and throws an error if instanceof returns false.
  // By default no modelTypes are defined.
  modelTypes: {
    user: UserModel,
    allUsers: UserCollection
  },

  // Enable access to the wrapped component's ref with the `withRef` option.
  // You can then access the wrapped component from the connected component's `getWrappedInstance()`.
  // This is similar to react-redux's connectAdvanced() HOC.
  // By default, `withRef` is false.
  withRef: true
};

const { connectBackboneToReact } = require("connect-backbone-to-react");

// Create our Connected Higher order Component (HOC).
const MyComponentConnected = connectBackboneToReact(
  mapModelsToProps,
  options
)(MyComponent);

Now that you've created your HOC you can use it!

// Map your Backbone Model and Collections to names that will be provided to
// your mapModelsToProps function.
const modelsMap = {
  user: userInstance,
  allUsers: userCollection
};

ReactDOM.render(
  // Pass the modelsMap to the HOC via the models prop.
  <MyComponentConnected models={modelsMap} showOnlyLaughingUsers={true} />,
  document.getElementById("app")
);

BackboneProvider

Alternatively you might have a tree of connected Components. We shouldn't pass that modelsMap object from one component to another. Instead we can take inspiration from react-redux's Provider component.

const { BackboneProvider } = require('connect-backbone-to-react');

const modelsMap = {
  user: userInstance,
  allUsers: userCollection,
},

ReactDOM.render(
  // Pass the modelsMap to the BackboneProvider via the models prop.
  // It will then get shared to every child connected component via React's context.
  <BackboneProvider models={modelsMap}>
    <MyComponentConnected>
      <MyComponentConnected />
    </MyComponentConnected>
  </BackboneProvider>,
  document.getElementById('app')
);

Rendering React Within Backbone.View

This library's focus is on sharing Backbone.Models with React Components. It is not concerned with how to render React Components within Backbone.Views. The React docs provide a possible implementation for this interopt.

Local development

To develop this library locally, run the following commands in the project root directory:

  1. npm run watch. The library will be automatically compiled in the background as you make changes.
  2. npm link and then follow the instructions to use the local version of this library in another project that uses connect-backbone-to-react.

Run npm test to run the unit tests.

Releasing a new version

  1. Make sure you have up to date node_modules before you proceed. Can be done via npm ci
  2. Update the version via: npm run release -- --release-as=major|minor|patch
  3. Optionally manually edit the revised CHANGELOG.md file. Commit changes.
  4. Follow the directions from step 2: run git push --follow-tags origin master; npm publish to publish
  5. Rejoice!

License

Apache 2.0

connect-backbone-to-react's People

Contributors

bradvogel avatar hswolff avatar jetpacmonkey avatar logandavis avatar spencer-brown avatar tewson avatar yasincanakmehmet avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

connect-backbone-to-react's Issues

RFC: Improved connectBackboneToReact and BackboneProvider

How do I RFC?

  • Read the Problem and Goals, then the proposed solution.
  • If you like the Proposed Solution, give a ๐Ÿ‘ or an LGTM.
  • If you dislike the proposed solution, check out the alternate solutions.
    • If you like one of them, propose one of them and explain why.
    • If you sort of like one of them, propose a change to it.
    • If you dislike all of them, propose a new alternate solution.
  • If you'd rather this be something we don't regulate, voice that.

Statuses for RFCs are:

  • Discussion
  • Adopted
  • Not Regulated

Contents

1. Problem

In order to use React Components within Backbone we need to accomplish two things:

  1. Be able to render a React Component within Backbone. (This is addressed in a PR).

After the PR is merged we will be able to render a React Component within a Marionette View like this:

class Greeting extends Component {
  render() {
    return (
      <div>
        Hello!
      </div>
    );
  }
}

const MyView = mixInto(Marionette.ItemView)(
    viewWithReact,
).extend({
    template: () => `
      <div>
          <header>
              <h1>Welcome!</h1>
          </header>

          <div class="react"></div>
      </div>
    `,

    initialize: function(options) {
    
      this.registerReactComponent(
        <Greeting />,
        '.react'
      );
    }
});

(new MyView()).render();
// Rendered!
  1. Be able to share data stored within Backbone Models and Collections between React Components and Backbone Views.

Consider this case:

class Greeting extends Component {
  render() {
    return (
      <div>
        Hello {this.props.name}!
      </div>
    );
  }
}

const MyView = mixInto(Marionette.ItemView)(
    viewWithReact,
).extend({
    template: () => `
      <div>
          <header>
              <h1>Welcome!</h1>
          </header>

          <div class="react"></div>
      </div>
    `,

    initialize: function(options) {
      const model = new Backbone.Model({
        name: 'Harry',
      });

      // We could do this:

      this.registerReactComponent(
        <Greeting name={model.get('name')} />,
        '.react'
      );

      // However if we then do:

      model.set('name', 'The Loud One');

      // Then the React Component is out of sync with the data.

      // This can be fixed via:

      model.on('change:name', () => {
        this.registerReactComponent(
          <Greeting name={model.get('name')} />,
          '.react'
        );

        this.render();
      });

      // But that quickly gets cumbersome.

      // Alternatively we could fix it like:
      this.registerReactComponent(
        <Greeting model={model} />,
        '.react'
      );

      // And within the Greeting component set up our event handlers directly,
      // but again that gets cumbersome to repeat that boilerplate every time.

      // Additionally a React Component should not have a direct dependence on a Model.
      // We should aim to decouple.
    }
});

(new MyView()).render();

This RFC addresses the second issue.

2. Goals

  1. Retain one true source of truth. That being, the data stored within the Models and Collections is what all UI should use to render.
  2. Create an abstraction between Backbone Models/Collections and React Components. This is to prevent a React Component from being directly dependent on a Model/Collection.
  3. Keep the UI in sync with the data in the Models/Collections. Such that whenever the Model/Collection changes the React Component re-renders.
  4. Create an API that is as frictionless as possible. This is to encourage usage.

3. Proposed Solution

An initial solution has been created via connect-backbone-to-react package, however the API has room for improvement before we mark it as 1.0.0.

This solution uses the Higher-Order Components (HoCs) pattern. This is the solution the React community has moved to after finding mixins hard to maintain. The end goal is the same as mixins, sharing functionality, however HoCs are easier to reason about. Refer to that document for further explanation.

connectBackboneToReact creates a HoC. The HoC returned:

  • Manages creating Model/Collection eventListeners so that when the model changes, the enclosed component is re-rendered.
  • Handles cleaning up eventListeners when the component is unmounted.
  • Maps Models/Collections to props that are given to your component.
  • Passes any additional props given to the HoC to the WrappedComponent. This allows you to still pass props that change non-data related behaviors, such as showPlaceholder={false}.

Ultimately we have four inputs required to correctly create our HoC:

  • modelsMap - this defines the map of Models/Collections to local names.

    • Example: { user: userModel, settings: settingsCollection }
  • mapModelsToProps - this is function that maps the values from modelsMap to props that will be given to our WrappedComponent.

    • Example:
      function({ user, settings }) {
        return {
          name: user.get('name'),
          changeName: (newName) => user.set('name', newName)
        };
      };
  • options - general options object to manipulate how the wrapper behaves.

    • options.debounce: Should our event handler function be wrapped in a debounce function. By default it's false.
    • options.events: Custom map of events our HoC should subscribe to. By default the all event is used for each Model/Collection.
    • options.modelTypes: Define what Model/Collection types the HoC expects to be given in the modelsMap. This is patterned off of React's PropTypes.
  • WrappedComponent - our own React Component that is going to be wrapped.

The full usage would look like:

// ComponentA.js
const { connectBackboneToReact } = require('connect-backbone-to-react');

class ComponentA extends Component {
  render() {
    return (
      <div>
        Hello {this.props.name}!
        <div
          onClick={() => {
            this.props.setName(Math.random());
          }}
      </div>
    );
  }
}

const mapModelsToProps = ({ user }) => {
  return {
    name: user.get('name'),
    setName: (newName) => user.set('name', newName)
  };
},
const options = {
  debounce: true,
  events: {
    user: ['change', 'reset'],
  },
  modelTypes: {
    user: Backbone.Model,
  },
};

module.exports = connectBackboneToReact(
  mapModelsToProps,  
  options
)(ComponentA);

// MyView.js

// ComponentA is the connected HoC.
const ComponentA = require('./ComponentA.js');

const MyView = mixInto(Marionette.ItemView)(
  viewWithReact,
).extend({
  template,

  initialize: function(options) {
    const model = new Backbone.Model({
      name: 'Harry',
    });

    const modelsMap = {
      user: model,
    };

    this.registerReactComponent(
      <ComponentA models={modelsMap} />,
      '.react'
    );
  }
});

That covers the basic use case of rendering one connected React Component. However as we incrementally migrate more Backbone Views to React we'll end up with a tree of React Components. So what happens if we have a tree of connected React Components?

class ComponentA extends Component {
  render() {
    return (
      <div>
        <ConnectedComponentB>
          <ConnectedComponentC />
        </ConnectedComponentB>
      </div>
    );
  }
}

We shouldn't pass that modelsMap object from one component to another. Instead we can take inspiration from react-redux's Provider component.

This allows us to share the modelsMap via React's Context feature.

Usage would look like:

// ComponentA is the connected HoC.
const ComponentA = require('./ComponentA.js');
const { BackboneProvider } = require('connect-backbone-to-react');

const MyView = mixInto(Marionette.ItemView)(
  viewWithReact,
).extend({
  template,

  initialize: function(options) {
    const model = new Backbone.Model({
      name: 'Harry',
    });

    const modelsMap = {
      user: model,
    };

    this.registerReactComponent(
      <BackboneProvider models={modelsMap}>
        <ComponentA />
      </BackboneProvider>,
      '.react'
    );
  }
});

BackboneProvider shares the modelsMap via context. Every HoC connected to Backbone would then retrieve the modelsMap via context.

4. Alternate Solutions

What should the props name be that the modelsMap is given to?

Right now you pass in modelsMap through the models prop:

<ComponentA models={modelsMap} />

Alternatively:

<ComponentA data={modelsMap} />

Where should we put modelTypes?

We have the proposed solution, here's an alternative which mirrors the style that PropTypes are added to React components.

const GreetingConnected = connectBackboneToReact(
  mapModelsToProps,
  options,
)(Greeting);

GreetingConnected.modelTypes = {
  user: instanceOf(UserModel),
  allUsers: instanceOf(UserCollection),
};

Alternative API

This is a discussion about what the API for this function should look like. The goal here is to maximize the efficiency of using this function.

To lay out the problem, right now we have a total of 4 arguments that are used:

  • modelsMap - this defines the map of Models & Collections to local names.

    • Example: { user: userModel, settings: settingsCollection }
  • modelsToProps - this is function that maps the values from modelsMap to properties that will be given to our WrappedReactComponent.

    • Example:
     function({ user, settings }) {
        return {
             name: user.get('name'),
             changeName: (newName) => user.set('name', newName)
        };
  • options - general options object to manipulate how the wrapper behaves.

  • WrappedComponent - our own React Component that is going to be wrapped.

And right now those 4 arguments are being used like this:

const ConnectedComponent = connectBackboneToReact(modelsMap, modelsToProps, options)(WrappedComponent);

The thought here being that you can predefine how models are being mapped to properties which you could easily add to any WrappedComponent.

So you could

const connectCreator = connectBackboneToReact(modelsMap, modelsToProps, options);
const ConnectedComponent = connectCreator(WrappedComponent)
const ConnectedComponent2 = connectCreator(WrappedComponent2)

@jrbalsano suggested an alternative API that I think is pretty good and possibly should replace the current one.

He suggests to change the argument syntax around to allow for easier exporting. Something close to:

const ConnectedComponent = connectBackboneToReact(modelsToProps, options)(WrappedComponent);

ReactDOM.render(
  <ConnectedComponent {...modelsMap} />.
  domEl
);

I'm kind of inclined with the 2nd way at this point as it allows for a user to define the props that a component will get, and it will work nicely alongside react-redux.

// file: MyComponent.js
class MyComponent extends Comonent {
    render() {
        return (
            <div>Hello Moto</div>
        );
    }
}

module.exports = MyComponent;

module.exports.MyComponentBackbone = connectBackboneToReact(
    modelsToProps,
    options
)(MyComponent);

module.exports.MyComponentRedux = connect(mapStateToProps)(MyComponent);

The current API doesn't really allow for that type of export signature.

Thoughts on changing to new API? Any alternative suggestions?

This is the only blocking IMO for pushing this to 1.0.0.

Problem with using Context in React Classes

Hi guys,

Thanks for sharing your plugin!

Ive recently upgraded React along with your plugin so I can take advantage of using React Context API. For functional components everything has worked fine. However if I use Context in a class component, which is also connected to your plugin via connectBackboneEvents HOC I get console errors when the model listeners are setup.

Uncaught TypeError: model.on is not a function createEventListener createEventListener createEventListeners createEventListeners ConnectBackboneToReact constructClassInstance

Debugging the code it seems that the context 'this.context' in my wrapped component is being used as the context in the HOC.
const models = Object.assign({}, this.context, this.props.models);
https://github.com/mongodb-js/connect-backbone-to-react/blob/master/lib/connect-backbone-to-react.js#L88
Screenshot 2020-10-29 at 17 03 21

Screenshot 2020-10-29 at 17 16 01

I know I can work my way around this by not using context in components that are using this HOC. However, that is a bit of a workaround, and it would be nice if we could fix the plugin to handle all scenarios. Perhaps reworking the validateModelTypes function to ensure only Backbone models are set to this.models.

Thanks!
Gary

mapModelsToProps should be given the props object as a second argument

This is to align better with how connect() from react-redux behaves.

This will allow usage of:

function mapModelsToProps(models, props) {
  // props is the props given to the HOC
  assert(props.name === 'harry'); // true;
}
const ConnectedComponent = connectBackboneToReact(mapModelsToProps)(MyComponent);

<ConnectedComponent name="harry" />

Using a ref from a class component

I have a class component that has a method inside of it:

class MyClassComponent extends Component {
  method1() {}
  render() {
    // ...
  }
}

And I have a snippet that saves the reference of the class component to be used later (doSomething):

render() {
  this._componentInstance = React.createRef();
  // Create and render the nav menu.
  ReactDOM.render(
    <MyClassComponent ref={this._componentInstance} />,
    domNode
  );
}

doSomething() {
  // calls the method from the instance
  this._componentInstance.current.method1();
}

That works because according to the docs,

When the ref attribute is used on a custom class component, the ref object receives the mounted instance of the component as its current.

Problem is, when I add the HoC, the ref stops working:

class MyClassComponent extends Component {
  method1() {}
  render() {
    // ...
  }
}
export default connectBackboneToReact(mapModelsToProps, { withRef: true })(MyClassComponent)

Then every time this._componentInstance.current.method1(); runs, it says the this._componentInstance.current is undefined.

State not updated when props passed to connected component change

Let's say component A is a connected component that's been wrapped by connect-backbone-to-react and component B is a component that renders component A. If component B's state changes in a way that changes the models passed down to connected component A from component B, those changes should propagate down to the underlying wrapped component.

Currently, this does not happen - the HOC's state is only set/updated when it's initialized and when the initially passed models/collections fire an event.

Wrapped components cannot be referenced via parent components.

Wrapped components can't be referenced directly, since the reference ends up being the connectBackboneToReact and not the wrapped component. Quick example:

class MyComponent extends Component {
  constructor() {
    super();
    this.ref = null;
  }

  render() {
    return (<WrappedComponent ref={(ref) => this.ref = ref}/>);
  }
}

Where WrappedComponent is wrapped by connect-backbone-react.

this.ref is a connectBackboneToReact object instead a WrappedComponent object.

react-redux fixes this by with an option withRef, see here: https://github.com/reduxjs/react-redux/blob/master/docs/api.md#connectadvancedselectorfactory-connectoptions

A similar pattern can allow connect-backbone-to-react to expose the wrapped object reference instead the wrapper itself.

`models` cannot be received from props *and* context

Based on the way context and props are handled here, models cannot be received from context and props simultaneously. props gets priority, knocking out any models passed via context. We had a discussion at @mixmaxhq about this today and concluded that it would probably make more sense for models received from context and props to be merged, eg with

const models = Object.assign({}, context.models, props.models);

---

I wanted to check in with y'all before submitting a PR to hear how this lines up with your usage. Does this change make sense to y'all too? If so, I'm happy to go ahead and make it!

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.