|
| 1 | +--- |
| 2 | +id: direct-manipulation |
| 3 | +title: Direct Manipulation |
| 4 | +layout: docs |
| 5 | +category: Guides |
| 6 | +permalink: docs/direct-manipulation.html |
| 7 | +next: linking-libraries |
| 8 | +--- |
| 9 | + |
| 10 | +It is sometimes necessary to make changes directly to a component |
| 11 | +without using state/props to trigger a re-render of the entire subtree. |
| 12 | +When using React in the browser for example, you sometimes need to |
| 13 | +directly modify a DOM node, and the same is true for views in mobile |
| 14 | +apps. `setNativeProps` is the React Native equivalent to setting |
| 15 | +properties directly on a DOM node. |
| 16 | + |
| 17 | +> Use setNativeProps when frequent re-rendering creates a performance bottleneck |
| 18 | +> |
| 19 | +> Direct manipulation will not be a tool that you reach for |
| 20 | +> frequently; you will typically only be using it for creating |
| 21 | +> continuous animations to avoid the overhead of rendering the component |
| 22 | +> hierarchy and reconciling many views. `setNativeProps` is imperative |
| 23 | +> and stores state in the native layer (DOM, UIView, etc.) and not |
| 24 | +> within your React components, which makes your code more difficult to |
| 25 | +> reason about. Before you use it, try to solve your problem with `setState` |
| 26 | +> and [shouldComponent](react-native/docs/direct-manipulation.html#setnativeprops-shouldcomponentupdate). |
| 27 | +
|
| 28 | +## setNativeProps with TouchableOpacity |
| 29 | + |
| 30 | +[TouchableOpacity](https://github.com/facebook/react-native/blob/master/Libraries/Components/Touchable/TouchableOpacity.js) |
| 31 | +uses `setNativeProps` internally to update the opacity of its child |
| 32 | +component: |
| 33 | + |
| 34 | +```javascript |
| 35 | +setOpacityTo: function(value) { |
| 36 | + // Redacted: animation related code |
| 37 | + this.refs[CHILD_REF].setNativeProps({ |
| 38 | + opacity: value |
| 39 | + }); |
| 40 | +}, |
| 41 | +``` |
| 42 | + |
| 43 | +This allows us to write the following code and know that the child will |
| 44 | +have its opacity updated in response to taps, without the child having |
| 45 | +any knowledge of that fact or requiring any changes to its implementation: |
| 46 | + |
| 47 | +```javascript |
| 48 | +<TouchableOpacity onPress={this._handlePress}> |
| 49 | + <View style={styles.button}> |
| 50 | + <Text>Press me!</Text> |
| 51 | + </View> |
| 52 | +<TouchableOpacity> |
| 53 | +``` |
| 54 | + |
| 55 | +Let's imagine that `setNativeProps` was not available. One way that we |
| 56 | +might implement it with that constraint is to store the opacity value |
| 57 | +in the state, then update that value whenever `onPress` is fired: |
| 58 | + |
| 59 | +```javascript |
| 60 | +getInitialState() { |
| 61 | + return { myButtonOpacity: 1, } |
| 62 | +}, |
| 63 | + |
| 64 | +render() { |
| 65 | + return ( |
| 66 | + <TouchableOpacity onPress={() => this.setState({myButtonOpacity: 0.5})} |
| 67 | + onPressOut={() => this.setState({myButtonOpacity: 1})}> |
| 68 | + <View style={[styles.button, {opacity: this.state.myButtonOpacity}]}> |
| 69 | + <Text>Press me!</Text> |
| 70 | + </View> |
| 71 | + </TouchableOpacity> |
| 72 | + ) |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +This is computationally intensive compared to the original example - |
| 77 | +React needs to re-render the component hierarchy each time the opacity |
| 78 | +changes, even though other properties of the view and its children |
| 79 | +haven't changed. Usually this overhead isn't a concern but when |
| 80 | +performing continuous animations and responding to gestures, judiciously |
| 81 | +optimizing your components can improve your animations' fidelity. |
| 82 | + |
| 83 | +If you look at the implementation of `setNativeProps` in |
| 84 | +[NativeMethodsMixin.js](https://github.com/facebook/react-native/blob/master/Libraries/ReactIOS/NativeMethodsMixin.js) |
| 85 | +you will notice that it is a wrapper around `RCTUIManager.updateView` - |
| 86 | +this is the exact same function call that results from re-rendering - |
| 87 | +see [receiveComponent in |
| 88 | +ReactNativeBaseComponent.js](https://github.com/facebook/react-native/blob/master/Libraries/ReactNative/ReactNativeBaseComponent.js). |
| 89 | + |
| 90 | +## Composite components and setNativeProps |
| 91 | + |
| 92 | +Composite components are not backed by a native view, so you cannot call |
| 93 | +`setNativeProps` on them. Consider this example: |
| 94 | + |
| 95 | +```javascript |
| 96 | +var MyButton = React.createClass({ |
| 97 | + render() { |
| 98 | + return ( |
| 99 | + <View> |
| 100 | + <Text>{this.props.label}</Text> |
| 101 | + </View> |
| 102 | + ) |
| 103 | + }, |
| 104 | +}); |
| 105 | + |
| 106 | +var App = React.createClass({ |
| 107 | + render() { |
| 108 | + return ( |
| 109 | + <TouchableOpacity> |
| 110 | + <MyButton label="Press me!" /> |
| 111 | + </TouchableOpacity> |
| 112 | + ) |
| 113 | + }, |
| 114 | +}); |
| 115 | +``` |
| 116 | +[Run this example](https://rnplay.org/apps/JXkgmQ) |
| 117 | + |
| 118 | +If you run this you will immediately see this error: `Touchable child |
| 119 | +must either be native or forward setNativeProps to a native component`. |
| 120 | +This occurs because `MyButton` isn't directly backed by a native view |
| 121 | +whose opacity should be set. You can think about it like this: if you |
| 122 | +define a component with `React.createClass` you would not expect to be |
| 123 | +able to set a style prop on it and have that work - you would need to |
| 124 | +pass the style prop down to a child, unless you are wrapping a native |
| 125 | +component. Similarly, we are going to forward `setNativeProps` to a |
| 126 | +native-backed child component. |
| 127 | + |
| 128 | +#### Forward setNativeProps to a child |
| 129 | + |
| 130 | +All we need to do is provide a `setNativeProps` method on our component |
| 131 | +that calls `setNativeProps` on the appropriate child with the given |
| 132 | +arguments. |
| 133 | + |
| 134 | +```javascript |
| 135 | +var MyButton = React.createClass({ |
| 136 | + setNativeProps(nativeProps) { |
| 137 | + this._root.setNativeProps(nativeProps); |
| 138 | + }, |
| 139 | + |
| 140 | + render() { |
| 141 | + return ( |
| 142 | + <View ref={component => this._root = component} {...this.props}> |
| 143 | + <Text>{this.props.label}</Text> |
| 144 | + </View> |
| 145 | + ) |
| 146 | + }, |
| 147 | +}); |
| 148 | +``` |
| 149 | +[Run this example](https://rnplay.org/apps/YJxnEQ) |
| 150 | + |
| 151 | +You can now use `MyButton` inside of `TouchableOpacity`! A sidenote for |
| 152 | +clarity: we used the [ref callback](https://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute) syntax here, rather than the traditional string-based ref. |
| 153 | + |
| 154 | +You may have noticed that we passed all of the props down to the child |
| 155 | +view using `{...this.props}`. The reason for this is that |
| 156 | +`TouchableOpacity` is actually a composite component, and so in addition |
| 157 | +to depending on `setNativeProps` on its child, it also requires that the |
| 158 | +child perform touch handling. To dot this, it passes on [various |
| 159 | +props](https://facebook.github.io/react-native/docs/view.html#onmoveshouldsetresponder) |
| 160 | +that call back to the `TouchableOpacity` component. |
| 161 | +`TouchableHighlight`, in contrast, is backed by a native view only |
| 162 | +requires that we implement `setNativeProps`. |
| 163 | + |
| 164 | +## Precomputing style |
| 165 | + |
| 166 | +We learned above that `setNativeProps` is a wrapper around |
| 167 | +`RCTUIManager.updateView`, which is also used internally by React to |
| 168 | +perform updates on re-render. One important difference is that |
| 169 | +`setNativeProps` does not call `precomputeStyle`, which is done |
| 170 | +internally by React, and so the `transform` property will not work if |
| 171 | +you try to update it manually with `setNativeProps`. To fix this, |
| 172 | +you can call `precomputeStyle` on your object first: |
| 173 | + |
| 174 | +```javascript |
| 175 | +var precomputeStyle = require('precomputeStyle'); |
| 176 | + |
| 177 | +var App = React.createClass({ |
| 178 | + componentDidMount() { |
| 179 | + var nativeProps = precomputeStyle({transform: [{rotate: '45deg'}]}); |
| 180 | + this._root.setNativeProps(nativeProps); |
| 181 | + }, |
| 182 | + |
| 183 | + render() { |
| 184 | + return ( |
| 185 | + <View ref={component => this._root = component} |
| 186 | + style={styles.container}> |
| 187 | + <Text>Precompute style!</Text> |
| 188 | + </View> |
| 189 | + ) |
| 190 | + }, |
| 191 | +}); |
| 192 | +``` |
| 193 | +[Run this example](https://rnplay.org/apps/8_mIAA) |
| 194 | + |
| 195 | +## setNativeProps to clear TextInput value |
| 196 | + |
| 197 | +Another very common use case of `setNativeProps` is to clear the value |
| 198 | +of a TextInput. The `controlled` prop of TextInput can sometimes drop |
| 199 | +characters when the `bufferDelay` is low and the user types very |
| 200 | +quickly. Some developers prefer to skip this prop entirely and instead |
| 201 | +use `setNativeProps` to directly manipulate the TextInput value when |
| 202 | +necessary. For example, the following code demonstrates clearing the |
| 203 | +input when you tap a button: |
| 204 | + |
| 205 | +```javascript |
| 206 | +var App = React.createClass({ |
| 207 | + clearText() { |
| 208 | + this._textInput.setNativeProps({text: ''}); |
| 209 | + }, |
| 210 | + |
| 211 | + render() { |
| 212 | + return ( |
| 213 | + <View style={styles.container}> |
| 214 | + <TextInput ref={component => this._textInput = component} |
| 215 | + style={styles.textInput} /> |
| 216 | + <TouchableOpacity onPress={this.clearText}> |
| 217 | + <Text>Clear text</Text> |
| 218 | + </TouchableOpacity> |
| 219 | + </View> |
| 220 | + ); |
| 221 | + } |
| 222 | +}); |
| 223 | +``` |
| 224 | +[Run this example](https://rnplay.org/plays/pOI9bA) |
| 225 | + |
| 226 | +## Avoiding conflicts with the render function |
| 227 | + |
| 228 | +If you update a property that is also managed by the render function, |
| 229 | +you might end up with some unpredictable and confusing bugs because |
| 230 | +anytime the component re-renders and that property changes, whatever |
| 231 | +value was previously set from `setNativeProps` will be completely |
| 232 | +ignored and overridden. [See this example](https://rnplay.org/apps/bp1DvQ) |
| 233 | +for a demonstration of what can happen if these two collide - notice |
| 234 | +the jerky animation each 250ms when `setState` triggers a re-render. |
| 235 | + |
| 236 | +## setNativeProps & shouldComponentUpdate |
| 237 | + |
| 238 | +By [intelligently applying |
| 239 | +`shouldComponentUpdate`](https://facebook.github.io/react/docs/advanced-performance.html#avoiding-reconciling-the-dom) |
| 240 | +you can avoid the unnecessary overhead involved in reconciling unchanged |
| 241 | +component subtrees, to the point where it may be performant enough to |
| 242 | +use `setState` instead of `setNativeProps`. |
0 commit comments