Skip to content

Commit

Permalink
Modernize code examples in the direct manipulation section (#2189)
Browse files Browse the repository at this point in the history
* Modernize code examples in the direct manipulation section.

* small code tweaks/fixes + run code through Prettier

Co-authored-by: Bartosz Kaszubowski <[email protected]>
  • Loading branch information
steveluscher and Simek authored Sep 1, 2020
1 parent f32d2ba commit a781893
Showing 1 changed file with 83 additions and 83 deletions.
166 changes: 83 additions & 83 deletions docs/direct-manipulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,20 @@ It is sometimes necessary to make changes directly to a component without using
[TouchableOpacity](https://github.com/facebook/react-native/blob/master/Libraries/Components/Touchable/TouchableOpacity.js) uses `setNativeProps` internally to update the opacity of its child component:

```jsx
setOpacityTo(value) {
const viewRef = useRef();
const setOpacityTo = useCallback((value) => {
// Redacted: animation related code
this.refs[CHILD_REF].setNativeProps({
viewRef.current.setNativeProps({
opacity: value
});
},
}, []);
```

This allows us to write the following code and know that the child will have its opacity updated in response to taps, without the child having any knowledge of that fact or requiring any changes to its implementation:

```jsx
<TouchableOpacity onPress={this._handlePress}>
<View style={styles.button}>
<TouchableOpacity onPress={handlePress}>
<View>
<Text>Press me!</Text>
</View>
</TouchableOpacity>
Expand All @@ -35,21 +36,16 @@ This allows us to write the following code and know that the child will have its
Let's imagine that `setNativeProps` was not available. One way that we might implement it with that constraint is to store the opacity value in the state, then update that value whenever `onPress` is fired:

```jsx
constructor(props) {
super(props);
this.state = { myButtonOpacity: 1, };
}

render() {
return (
<TouchableOpacity onPress={() => this.setState({myButtonOpacity: 0.5})}
onPressOut={() => this.setState({myButtonOpacity: 1})}>
<View style={[styles.button, {opacity: this.state.myButtonOpacity}]}>
<Text>Press me!</Text>
</View>
</TouchableOpacity>
)
}
const [buttonOpacity, setButtonOpacity] = useState(1);
return (
<TouchableOpacity
onPressIn={() => setButtonOpacity(0.5)}
onPressOut={() => setButtonOpacity(1)}>
<View style={{ opacity: buttonOpacity }}>
<Text>Press me!</Text>
</View>
</TouchableOpacity>
);
```

This is computationally intensive compared to the original example - React needs to re-render the component hierarchy each time the opacity changes, even though other properties of the view and its children haven't changed. Usually this overhead isn't a concern but when performing continuous animations and responding to gestures, judiciously optimizing your components can improve your animations' fidelity.
Expand All @@ -61,86 +57,90 @@ If you look at the implementation of `setNativeProps` in [NativeMethodsMixin](ht
Composite components are not backed by a native view, so you cannot call `setNativeProps` on them. Consider this example:

```SnackPlayer name=setNativeProps%20with%20Composite%20Components
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
const MyButton = (props) => {
return (
<View style={{marginTop: 50}}>
<Text>{props.label}</Text>
</View>
)
}
export default App = () => {
return (
<TouchableOpacity>
<MyButton label="Press me!" />
</TouchableOpacity>
)
}
import React from "react";
import { Text, TouchableOpacity, View } from "react-native";
const MyButton = (props) => (
<View style={{ marginTop: 50 }}>
<Text>{props.label}</Text>
</View>
);
export default App = () => (
<TouchableOpacity>
<MyButton label="Press me!" />
</TouchableOpacity>
);
```

If you run this you will immediately see this error: `Touchable child must either be native or forward setNativeProps to a native component`. This occurs because `MyButton` isn't directly backed by a native view whose opacity should be set. You can think about it like this: if you define a component with `createReactClass` you would not expect to be able to set a style prop on it and have that work - you would need to pass the style prop down to a child, unless you are wrapping a native component. Similarly, we are going to forward `setNativeProps` to a native-backed child component.

#### Forward setNativeProps to a child

All we need to do is provide a `setNativeProps` method on our component that calls `setNativeProps` on the appropriate child with the given arguments.
Since the `setNativeProps` method exists on any ref to a `View` component, it is enough to forward a ref on your custom component to one of the `<View />` components that it renders. This means that a call to `setNativeProps` on the custom component will have the same effect as if you called `setNativeProps` on the wrapped `View` component itself.

```SnackPlayer name=Forwarding%20setNativeProps
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
const MyButton = (props) => {
setNativeProps = (nativeProps) => {
_root.setNativeProps(nativeProps);
}
return (
<View style={{marginTop: 50}} ref={component => _root = component} {...props}>
<Text>{props.label}</Text>
</View>
)
}
export default App = () => {
return (
<TouchableOpacity>
<MyButton label="Press me!" />
</TouchableOpacity>
)
}
import React from "react";
import { Text, TouchableOpacity, View } from "react-native";
const MyButton = React.forwardRef((props, ref) => (
<View {...props} ref={ref} style={{ marginTop: 50 }}>
<Text>{props.label}</Text>
</View>
));
export default App = () => (
<TouchableOpacity>
<MyButton label="Press me!" />
</TouchableOpacity>
);
```

You can now use `MyButton` inside of `TouchableOpacity`! A sidenote for clarity: we used the [ref callback](https://reactjs.org/docs/refs-and-the-dom.html#adding-a-ref-to-a-dom-element) syntax here, rather than the traditional string-based ref.
You can now use `MyButton` inside of `TouchableOpacity`!

You may have noticed that we passed all of the props down to the child view using `{...this.props}`. The reason for this is that `TouchableOpacity` is actually a composite component, and so in addition to depending on `setNativeProps` on its child, it also requires that the child perform touch handling. To do this, it passes on [various props](view.md#onmoveshouldsetresponder) that call back to the `TouchableOpacity` component. `TouchableHighlight`, in contrast, is backed by a native view and only requires that we implement `setNativeProps`.
You may have noticed that we passed all of the props down to the child view using `{...props}`. The reason for this is that `TouchableOpacity` is actually a composite component, and so in addition to depending on `setNativeProps` on its child, it also requires that the child perform touch handling. To do this, it passes on [various props](view.md#onmoveshouldsetresponder) that call back to the `TouchableOpacity` component. `TouchableHighlight`, in contrast, is backed by a native view and only requires that we implement `setNativeProps`.

## setNativeProps to clear TextInput value

Another very common use case of `setNativeProps` is to clear the value of a TextInput. The `controlled` prop of TextInput can sometimes drop characters when the `bufferDelay` is low and the user types very quickly. Some developers prefer to skip this prop entirely and instead use `setNativeProps` to directly manipulate the TextInput value when necessary. For example, the following code demonstrates clearing the input when you tap a button:

```SnackPlayer name=Clear%20text
import React from 'react';
import { TextInput, Text, TouchableOpacity, View } from 'react-native';
export default App = () => {
clearText = () => {
_textInput.setNativeProps({text: ''});
}
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<TextInput
ref={component => _textInput = component}
style={{height: 50, width: 200, marginHorizontal: 20, borderWidth: 1, borderColor: '#ccc'}}
/>
<TouchableOpacity onPress={clearText}>
<Text>Clear text</Text>
</TouchableOpacity>
</View>
);
}
import React from "react";
import { useCallback, useRef } from "react";
import { StyleSheet, TextInput, Text, TouchableOpacity, View } from "react-native";
const App = () => {
const inputRef = useRef();
const clearText = useCallback(() => {
inputRef.current.setNativeProps({ text: "" });
}, []);
return (
<View style={styles.container}>
<TextInput ref={inputRef} style={styles.input} />
<TouchableOpacity onPress={clearText}>
<Text>Clear text</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
input: {
height: 50,
width: 200,
marginHorizontal: 20,
borderWidth: 1,
borderColor: "#ccc",
},
});
export default App;
```

## Avoiding conflicts with the render function
Expand Down

0 comments on commit a781893

Please sign in to comment.