- React and React Native
- Adam Boduch
- 812字
- 2021-07-09 19:28:57
Rendering imperative components
Everything you've rendered so far in this book has been straightforward declarative HTML. As you know, life is never so simple: sometimes our React components need to implement some imperative code under the covers.
This is the key—hiding the imperative operations so that the code that renders your component doesn't have to touch it. In this section, you'll implement a simple jQuery UI button React component so that you can see how the relevant lifecycle methods help us encapsulate imperative code.
Rendering jQuery UI widgets
The jQuery UI widget library implements several widgets on top of standard HTML. It uses a progressive enhancement technique whereby the basic HTML is enhanced in browsers that support newer features. To make these widgets work, you first need to render HTML into the DOM somehow; then, make imperative function calls to create and interact with the widgets.
In this example, we'll create a React button component that acts as a wrapper around the jQuery UI widget. Anyone using the React component shouldn't need to know that behind the scenes, it's making imperative calls to control the widget. Let's see what the button component looks like:
import React, { Component } from 'react'; // Import all the jQuery UI widget stuff... import $ from 'jquery'; import 'jquery-ui/ui/widgets/button'; import 'jquery-ui/themes/base/all.css'; export default class MyButton extends Component { // When the component is mounted, we need to // call "button()" to initialize the widget. componentDidMount() { $(this.button).button(this.props); } // After the component updates, we need to use // "this.props" to update the options of the // jQuery UI button widget. componentDidUpdate() { $(this.button).button('option', this.props); } // Renders the "<button>" HTML element. The "onClick()" // handler will always be a assigned, even if it's a // noop function. The "ref" property is used to assign // "this.button". This is the DOM element itself, and // it's needed by the "componentDidMount()" and // "componentDidUpdate()" methods. render() { return ( <button onClick={this.props.onClick} ref={(button) => { this.button = button; }} /> ); } }
The jQuery UI button widget expects a <button>
element, so this is what's rendered by the component. An onClick()
handler is assigned as well, and this function is expected to be found in the properties. There's also a ref
property used here, which assigns the button
argument to this.button
. The reason this is done is so that the component has direct access to the underlying DOM element of the component. Generally, components don't need access to any DOM elements, but here, we need to issue imperative commands to the element.
For example, in the componentDidMount()
method, we call the button()
function and pass it properties from the component. We do something similar in the componentDidUpdate()
method, which is called when property values change. Now, let's take a look at the button container component:
import React, { Component } from 'react'; import { fromJS } from 'immutable'; import MyButton from './MyButton'; class MyButtonContainer extends Component { // The initial state is an empty Immutable map, because // by default, we won't pass anything to the jQuery UI // button widget. state = { data: fromJS({}), } // Getter for "Immutable.js" state data... get data() { return this.state.data; } // Setter for "Immutable.js" state data... set data(data) { this.setState({ data }); } // Before the component is mounted for the first time, // we have to bind the "onClick()" handler to "this" // so that the handler can set the state. componentWillMount() { this.data = this.data .merge(this.props, { onClick: this.props.onClick.bind(this), }); } // Renders the "<MyButton>" component with this // component's state as properties. render() { return ( <MyButton {...this.state.data.toJS()} /> ); } } // By default, the "onClick()" handler is a noop. // This makes it easier because we can always assign // the event handler to the "<button>". MyButtonContainer.defaultProps = { onClick: () => {}, }; export default MyButtonContainer;
Once again, we have a container component that controls the state, which is then passed to <MyButton>
as properties. The component has a default onClick()
handler function. But, as you can see in the componentWillMount()
method, we can pass a different click handler in as a property. Additionally, it's automatically bound to the component context, which is useful if the handler needs to change the button state. Let's look at an example of this:
import React from 'react'; import { render } from 'react-dom'; import MyButtonContainer from './MyButtonContainer'; // Simple button event handler that changes the // "disabled" state when clicked. function onClick() { this.data = this.data .set('disabled', true); } render(( <section> { /* A simple button with a simple label. */ } <MyButtonContainer label="Text" /> { /* A button with an icon, and a hidden label. */ } <MyButtonContainer label="My Button" icon="ui-icon-person" showLabel={false} /> { /* A button with a click event handler. */ } <MyButtonContainer label="Disable Me" onClick={onClick} /> </section> ), document.getElementById('app') );
Here, we have three jQuery UI button widgets, each controlled by a React component with no imperative code in sight. Here's how the buttons look: