New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add guide on integrating with non-react code #9316
Changes from 23 commits
675c0bb
995b2ba
ec88f4b
687edf3
9f2c2c7
a3a1934
236deef
aee0690
ba315b2
73f58b2
b0047ae
1077a1c
8397119
5c2f363
2c778e7
933fd5d
8f7e2bb
2fb9b30
541002a
00bb018
e9bd247
7811e88
cc92dc6
ac2134a
d52245f
931b051
4acd3fd
76f42e5
f1758eb
f0a4e02
6f6a9d2
4bf0523
ccb18fc
8967b35
74a6463
d9bc886
e502df1
75b0abd
38dd65c
9d2354e
8a643b3
92a1c37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
--- | ||
id: integrating-with-other-libraries | ||
title: Integrating with other libraries | ||
permalink: docs/integrating-with-other-libraries.html | ||
--- | ||
|
||
React can be used in any web application. It can be embedded in other applications and, with a little care, other applications can be embedded in React. This guide will examine some of the more common use cases, focusing on integration with jQuery and Backbone. | ||
|
||
## Integrating with DOM Manipulation Plugins | ||
|
||
React is unaware of changes made to the DOM outside of React. It determines updates based on its own internal representation, and if those updates are invalidated, React has no way to recover. | ||
|
||
This does not mean it is impossible or even necessarily difficult to combine React with other ways of affecting the DOM, you just have to be mindful of what each are doing. | ||
|
||
The easiest way to avoid conflicts is to prevent the React component from updating. This can be done explicitly by returning false from [`shouldComponentUpdate()`](/react/docs/react-component.html#shouldcomponentupdate), or by rendering elements that have no reason to change. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "that has no reason to change" is not really explained. Maybe add "like an empty There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And I see that you explained it later, but generally people stumble where they're confused, and don't read past it because they're not sure if they'll be able to follow the rest. It's better to explain before rather than after the code. |
||
|
||
```js | ||
class SomePlugin extends React.Component { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We jump into this code example too fast. It's worth adding a sentence before, e.g. "For example, a component rendering a jQuery plugin could look like this:" |
||
componentDidMount() { | ||
this.$el = $(this.el); | ||
this.$el.somePlugin(); | ||
} | ||
|
||
componentWillUnmount() { | ||
this.$el.somePlugin('destroy'); | ||
} | ||
|
||
render() { | ||
return <div ref={el => this.el = el} /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: missing semicolon at the end of the line |
||
} | ||
} | ||
``` | ||
A [ref](/react/docs/refs-and-the-dom.html) is used to pass the underlying DOM element to the plugin. The `<div>` element has no properties or children, so React has no reason to update it, leaving the plugin free to update the DOM without conflicts. | ||
|
||
The component still has to be unmounted, which provides one final opportunity for conflict. If the plugin does not provide a method for cleanup, you will probably have to provide your own, remembering to remove any event listeners the plugin registered to prevent memory leaks. | ||
|
||
To demonstrate these concepts let's write a minimal wrapper for the plugin [Chosen](https://github.com/harvesthq/chosen), which augments `<select>` inputs. | ||
|
||
Chosen does not render the initial select, so it is up to React to do so. Chosen hides the initial select and creates its own control, notifying the original of changes using jQuery. Because it is Chosen maintaining the state, it is easiest to implement the wrapper using an [uncontrolled component.](/react/docs/uncontrolled-components.html) | ||
|
||
```js | ||
class Chosen extends React.Component { | ||
constructor(props) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this work without |
||
super(props); | ||
this.handleUpdate = this.handleUpdate.bind(this); | ||
} | ||
|
||
handleUpdate(event) { | ||
this.props.update(event.target.value); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is |
||
} | ||
|
||
componentDidMount() { | ||
this.$el = $(this.el); | ||
this.$el.chosen(); | ||
this.$el.on('change', this.handleUpdate); | ||
} | ||
|
||
componentDidUpdate() { | ||
this.$el.trigger('chosen:updated'); | ||
} | ||
|
||
componentWillUnmount() { | ||
this.$el.off('change', this.handleUpdate); | ||
this.$el.chosen('destroy'); | ||
} | ||
|
||
render() { | ||
return ( | ||
<select ref={el => this.el = el}> | ||
{this.props.children} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens when children change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm worried that this example is potentially dangerous because we're mixing React children with jQuery parent with React grandparents. Maybe it would be better to use a "leaf" component for a jQuery example. It's also worth noting that generally it is recommended to use React components, and this is just an escape hatch for when you need to integrate with older code. |
||
</select> | ||
); | ||
} | ||
} | ||
``` | ||
|
||
1. Change listeners need to be setup inside `componentDidMount()` and torn down in `componentWillUnmount` as the events are emitted by the plugin use jQuery, which is outside the React event system. | ||
|
||
2. Chosen needs to be notified of any changes to the original select or its children. This is done in `componentWillUpdate()` by triggering a `'chosen:updated'` event. | ||
|
||
3. When the component is unmounted, use the cleanup method provided by Chosen. This removes the custom select control and restores the actual `<select>` as well as removing any event listeners the plugin registered. | ||
|
||
[Try it on CodePen.](http://codepen.io/wacii/pen/ygzxjG?editors=0010) | ||
|
||
## Integrating with Other View Libraries | ||
|
||
React can be easily embedded into other applications thanks to the flexibility of `ReactDOM.render()`. While often used once at startup to load a single React application into the DOM, `ReactDOM.render()` can be called multiple times, both to create multiple React applications and to update existing ones. This allows applications to be rewritten in React piece by piece. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth adding this is exactly how we use React at Facebook. |
||
|
||
### Replacing String Based Rendering with React | ||
|
||
A common pattern in older web applications is to describe chunks of the DOM as a string and insert it into the DOM like so: `$el.html(htmlString)`. These points in a codebase are perfect for introducing React. Just rewrite the string based rendering as a React component. | ||
|
||
So the following jQuery implementation... | ||
```js | ||
const htmlString = '<button id="jquery-btn">jQuery</button>'; | ||
$el.html(htmlString); | ||
$('jquery-btn').click(() => window.alert('jQuery button')); | ||
``` | ||
|
||
...could be rewritten using a React Component. | ||
```js | ||
function Button() { | ||
return <button id='react-btn'>React</button>; | ||
} | ||
ReactDOM.render( | ||
<Button />, | ||
document.getElementById('react-container'), | ||
() => { | ||
$('#react-btn').click(() => window.alert('React button')); | ||
} | ||
); | ||
``` | ||
|
||
From here you could start moving more logic into the component and begin adopting more common React practices. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe explicitly say "For example, in components it is best not to rely on IDs because the same component can be rendered multiple times. Instead, we will use React event system:" and link that to events doc. |
||
|
||
```js | ||
function Button({ handleClick }) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We avoid destructuring here in favor of |
||
return <button onClick={handleClick}>React</button>; | ||
} | ||
ReactDOM.render( | ||
<Button handleClick={() => window.alert('React button v2')} />, | ||
document.getElementById('react-container') | ||
); | ||
``` | ||
|
||
[Try it on CodePen.](http://codepen.io/wacii/pen/RpvYdj?editors=1010) | ||
|
||
### Embedding React in a Backbone View | ||
|
||
Backbone Views typically use HTML strings, or string producing template functions, to create the content for their associated DOM element. Like before, this process may be replaced with a React component. | ||
|
||
Each view will have an associated component, and when the view renders, `ReactDOM.render()` is used to render the component into the view's `el`. | ||
|
||
```js | ||
function Component({ text }) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also let's not call anything |
||
return <p>{text}</p>; | ||
} | ||
|
||
const ComponentView = Backbone.View.extend({ | ||
render() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For people unfamiliar with Backbone, worth adding a comment here that notes that in Backbone, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a section about rendering with strings/templates so the |
||
const text = this.model.get('text'); | ||
ReactDOM.render(<Component text={text} />, this.el); | ||
return this; | ||
}, | ||
remove() { | ||
ReactDOM.unmountComponentAtNode(this.el); | ||
Backbone.View.prototype.remove.call(this); | ||
} | ||
}); | ||
``` | ||
|
||
State is maintained in the view as `this.model` and passed to the component as props. | ||
|
||
In addition to normal cleanup, event listeners registered through React as well as component state should be removed by calling `ReactDOM.unmountComponentAtNode()`. This is something React normally calls for us, but because we are controlling the application with Backbone, we must do this manually to avoid leaking memory. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is ambigious and might give the wrong idea that you don't need to call We should make it clearer that |
||
|
||
[Try it on CodePen.](http://codepen.io/wacii/pen/OWZJMQ?editors=0010) | ||
|
||
## Integrating with Model Layers | ||
|
||
React has little opinion on how data is stored or updated and so React components can easily incorporate the model layer from other frameworks. Models may be consumed as is or extracted using containers for better separation of concerns. Both approaches will be demonstrated using Backbone. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe worth adding that in general we recommend one directional data flow such as React state, Flux, or Redux, but other approaches work too. |
||
|
||
### Using Backbone Models in React components | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just wanted to share some work I've done on this front. I recently created a HoC for syncing Backbone Models/Collections with React that might be of value to link to in this doc? I called it connect-backbone-to-react and its API is super inspired by react-redux's. Hopefully this is of value to other developers! |
||
|
||
The simplest way to consume Backbone models and collections from a React component is to listen to the various change events and manually force an update. | ||
|
||
Components responsible for rendering models would listen to `'change'` events, while components responsible for rendering collections would listen for `'add'` and `'remove` events. In both cases, call `this.forceUpdate()` to rerender the component with the new data. | ||
|
||
In the following code, `ListComponent` renders a collection with `ItemComponent` responsible for rendering the individual models. | ||
|
||
```js | ||
class ItemComponent extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.rerender = () => this.forceUpdate(); | ||
} | ||
|
||
componentDidMount() { | ||
this.props.model.on('change', this.rerender); | ||
} | ||
|
||
componentWillUnmount() { | ||
this.props.model.off('change', this.rerender); | ||
} | ||
|
||
render() { | ||
return <li>{this.props.model.get('text')}</li>; | ||
} | ||
} | ||
|
||
class ListComponent extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.rerender = () => this.forceUpdate(); | ||
} | ||
|
||
componentDidMount() { | ||
this.props.collection.on('add', 'remove', this.rerender); | ||
} | ||
|
||
componentWillUnmount() { | ||
this.props.collection.off('add', 'remove', this.rerender); | ||
} | ||
|
||
render() { | ||
return ( | ||
<ul> | ||
{this.props.collection.map(model => ( | ||
<ItemComponent key={model.cid} model={model} /> | ||
))} | ||
</ul> | ||
) | ||
} | ||
} | ||
``` | ||
|
||
[Try it on CodePen.](http://codepen.io/wacii/pen/oBdgoM?editors=0010) | ||
|
||
### Extracting data from Backbone models | ||
|
||
Alternatively, whenever a model changes, you can extract its attributes as plain data. The following is [a higher-order component (HOC)](/react/docs/higher-order-components.html) that extracts all attributes of a Backbone model into state, passing the data to the wrapped component. | ||
|
||
This way, only the HOC needs to know about Backbone model internals, and the components concerned with presenting data can focus on that. | ||
|
||
```js | ||
function backboneModelAdapter(Component) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe |
||
return class extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = { ...props.model.attributes }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is object rest/spread which isn't in ES yet. Can we use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if there's a different |
||
this.handleChange = this.handleChange.bind(this); | ||
} | ||
|
||
handleChange(model) { | ||
this.setState(model.changedAttributes()); | ||
} | ||
|
||
componentDidMount() { | ||
this.props.model.on('change', this.handleChange); | ||
} | ||
|
||
componentWillUnmount() { | ||
this.props.model.off('change', this.handleChange); | ||
} | ||
|
||
render() { | ||
const { model, ...otherProps } = this.props; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same on this line, this is object rest/spread and we probably shouldn't use it here yet. |
||
return <Component {...otherProps} {...this.state} />; | ||
} | ||
} | ||
} | ||
``` | ||
|
||
A copy is made of the model's attributes to form the initial state. Every time there is a change event, just the changed attributes are updated. | ||
|
||
To demonstrate its use, we will use a basic input wrapper. The component will render the `'text'` attribute of the provided model, calling an update function whenever the input is changed, presumably to update the model. | ||
|
||
```js | ||
function Input({ text, handleChange }) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return <input value={text} onChange={handleChange} />; | ||
} | ||
|
||
function BackboneModelAdapterDemo() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not super clear this is a component. Can you call this |
||
const model = new Backbone.Model({ text: 'Sam' }); | ||
const handleChange = event => model.set('text', event.target.value); | ||
const WrappedInput = backboneModelAdapter(Input); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't do this inside the component. You're creating a new copy each render, destroying the state and DOM. |
||
return <WrappedInput model={model} handleChange={handleChange} />; | ||
} | ||
``` | ||
|
||
[Try it on CodePen.](http://codepen.io/wacii/pen/RKyWKZ?editors=0010) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a one-paragraph summary for this page? Something that says that React can be used in any application, and summarizing two methods below.